# BellSoft Liberica JDK https://bell-sw.com BellSoft provides Liberica JDK — certified, supported OpenJDK builds for Linux, Windows, macOS and embedded platforms. Includes Java 8, 11, 17, 21 and more. ## Products - [10 Things to Consider Before Renewing Oracle License | BellSoft Java](https://bell-sw.com/10-things-to-consider-before-renewing-oracle-license/): Learn how Oracle's changing Java licensing models impact your business. Download our comprehensive guide to make informed decisions about renewing your Oracle license. - [20 Questions to Ask Your Potential OpenJDK Vendor | BellSoft Java](https://bell-sw.com/20-questions-to-ask-your-potential-open-jdk-vendor/): Confused about selecting the right OpenJDK distribution provider? Download our guide with 20 essential questions to make an informed decision. - [Page Not Found | BellSoft Java](https://bell-sw.com/404/): - [Discover the Seven Key Benefits of Liberica JDK | BellSoft Java](https://bell-sw.com/7-reasons-to-choose-liberica-jdk/): Looking for a Oracle Java Alternative? Download our guide to explore the seven key benefits of Liberica JDK, a 100% open-source and TCK-verified Java runtime. - [BellSoft Java Applications Developers highly qualified IT experts | BellSoft Java](https://bell-sw.com/about/): We are a team of highly qualified IT industry professionals with vast experience in building software products and technologies. We work for the largest multinational companies. We are contributors to the leading software technologies. - [Alpaquita container images | BellSoft Java](https://bell-sw.com/alpaquita-containers/): BellSoft provides ready Alpaquita containers on Docker Hub, GHCR, MCR, GCR and Amazon ECR. - [Alpaquita Containers for Spring Boot | BellSoft Java](https://bell-sw.com/alpaquita-containers-for-spring-boot-apps/): Alpaquita Containers for Spring Boot applications help to improve performance up to 30 %. - [Alpaquita Linux — A Minimal and Secure Linux Distribution for Java, Cloud, and Containers](https://bell-sw.com/alpaquita-linux/): Alpaquita Linux is the Alpine-based build fine-tuned for Java with glibc and musl support. Ideal for cloud deployment, containers, and native images. - [Technical Support for Alpaquita Linux | Bellsoft](https://bell-sw.com/alpaquita-support/): With the BellSoft support team, you can focus on deploying infrastructure or services with the utmost level of security in your containerized project. - [Alpaquita End User License Agreement](https://bell-sw.com/alpaquita_eula/): - [BellSoft Hardened Container Images | BellSoft Java](https://bell-sw.com/bellsoft-hardened-container-images/): BellSoft provides hardened container images designed for production environments that require enhanced security, predictable vulnerability management, and transparent build processes. These minimal, regularly updated images come with a comprehensive Security Advisory and represent a unified vendor approach to container hardening. - [BellSoft Hardened Images - Near Zero CVE Container Images | BellSoft Java](https://bell-sw.com/bellsoft-hardened-images/): Deploy secure minimized container images with near-zero CVEs. BellSoft Hardened Images deliver continuous security monitoring, continuous rebuilds, and easy compliance. - [BellSoft Hardened Container Images | BellSoft Java](https://bell-sw.com/bellsoft-hardened-images/vulnerability-comparisons/): BellSoft provides hardened container images designed for production environments that require enhanced security, predictable vulnerability management, and transparent build processes. These minimal, regularly updated images come with a comprehensive Security Advisory and represent a unified vendor approach to container hardening. - [Careers at BellSoft | BellSoft Java](https://bell-sw.com/careers/): BellSoft is a major OpenJDK contributor providing Progressive Java Runtime and other tools for Java apps for modern architectures and cloud. - [Alpaquita vs Alpine comparison | BellSoft Java](https://bell-sw.com/choose-an-optimal-linux-for-the-cloud/): Compare the key features of two lightweight Linux distributions, Alpine and Alpaquita - [Cloud Native Platform | BellSoft Java](https://bell-sw.com/cloud-native-platform/): Alpaquita Cloud Native Platform is the solution for cloud-native Java apps including lightweight Linux and Java runtime, Java tools, and support. - [Comparison Matrix of Popular Linux Distributions | BellSoft Java](https://bell-sw.com/comparison-of-popular-linux-distributions-for-the-cloud/): Confused about which Linux distro to choose for your Java environment? Download our comprehensive comparison table to find the perfect fit based on size, support, and features. - [Bellsoft’s Content Library](https://bell-sw.com/content-library/): Explore a diverse collection of guides, product briefs, e-books, webinars, and videos, all crafted to enhance your understanding and skills in Java programming. - [Join the raffle and win a Steam Deck | BellSoft Java](https://bell-sw.com/devnexus/): Fill in the form to join a raffle for winning a PlayStation 5 - [Join the raffle and win a Beats Studio Pro | BellSoft Java](https://bell-sw.com/devopsday-los-angeles/): Fill in the form to join a raffle for winning a Beats Studio Pro - [Join the raffle and win a Beats Studio Pro | BellSoft Java](https://bell-sw.com/devopsdays-chicago/): Fill in the form to join a raffle for winning a Beats Studio Pro - [Complete our survey for a chance to win a Steam Deck | BellSoft Java](https://bell-sw.com/devoxx-survey/): Complete our survey to join a raffle for winning a Steam Deck - [How to migrate to JDK with minimum effort | BellSoft Java](https://bell-sw.com/effortless-java-upgrade-made-possible/): Discover a solution transforming JDK upgrade into a smooth journey with minimum expenses - [BellSoft at Linaro Connect 19 BKK | BellSoft Java](https://bell-sw.com/events/2019/02/13/Events-Linaro-Connect-BKK/): BellSoft at Linaro Connect 19 BKK Dmitry Chuyko, BKK19-203 JVM on ARM. From sensor to cloud A variety of Java virtual machines on ARM have been around for a long time. - [BellSoft at JBCNConf in Barcelona | BellSoft Java](https://bell-sw.com/events/2019/04/11/Events-JBCNConf/): BellSoft at JBCNConf in Barcelona Dmitry Chuyko, Do not put all eggs in one container Microservice architecture and containerization have become the standards of modern application development. - [BellSoft at Oracle CodeOne 2019 in San Francisco | BellSoft Java](https://bell-sw.com/events/2019/06/05/Events-CodeOne2019/): BellSoft at Oracle CodeOne 2019 Dmitry Chuyko and Alex Belokrylov, Do not put all eggs in one container We will be happy to meet you at Oracle CodeOne conference in San Francisco, September 15 - 19. It is our honor to share the knowledge and… - [Meet a BellSoft performance engineer at Devoxx Belgium | BellSoft Java](https://bell-sw.com/events/2019/10/03/Events-Devoxx-Belgium-2019/): Meet BellSoft performance engineer at Devoxx Belgium Java on ARM. Theory, Applications and Workloads, Dmitry Chuyko, Wednesday from 15:10 to 16:00 in Room 6 There are many great events for developers around the world. - [Meet BellSoft engineers at JFokus in Stockholm | BellSoft Java](https://bell-sw.com/events/2020/02/02/Events-JFokus-Stockholm/): Meet BellSoft engineers at JFokus in Stockholm Check how small can be a Docker container with fully functional JDK at BellSoft Booth. Find us on February 4-5, 2020... - [Get savvy about native image at JRush | BellSoft Java](https://bell-sw.com/events/2021/03/25/Events-JRush/): In the light of the Liberica NIK release, BellSoft invites high-level software developers and Java enthusiasts to discover native image. Join us on March 25 at 11 am PDT online. Learn how this wonderful tool can optimize your projects and minimize resources! - [A web conference for DevOps engineers | BellSoft Java](https://bell-sw.com/events/2021/09/13/Events-JRush/): Dive into the latest DevOps trends and cutting-edge technologies. Find out how to increase the efficiency of your processes and accelerate the infrastructure transformation. - [A web conference for senior engineers | BellSoft Java](https://bell-sw.com/events/2021/11/25/Events-JRush/): Evolve your Java apps with modern security and performance tools. Find out how to minimize migrations and upgrades and make your software flexible and ever-viable. - [IANA Updater Update Time Zones information in Liberica JDK/JRE | BellSoft Java](https://bell-sw.com/iana-updater/): IANAUpdater is tool that can be used to update Time Zones information in Liberica JDK/JRE and, possilby, other OpenJDK-based distributions. - [BellSoft. Secure Liberica JDK and modern solutions for Java apps | BellSoft Java](https://bell-sw.com/): BellSoft is a major OpenJDK contributor providing Progressive Java Runtime and other tools for Java apps for modern architectures and cloud. - [The status of Java on Arm | BellSoft Java](https://bell-sw.com/java/arm/performance/2019/01/15/the-status-of-java-on-arm/): Today, as Arm processors are primarily viewed as targeting the embedded market, and justifiably so, multiple hardware vendors are using this architecture to build server CPUs and to compete with Intel in the cloud and High Performance Computing (HPC) segment. - [A guide to cloud cost optimization | BellSoft Java](https://bell-sw.com/java-cloud-deployment-cost-optimization/): Discover a comprehensive strategy on reducing cloud expenses for good - [Join the raffle and win a Steam Deck | BellSoft Java](https://bell-sw.com/java-forum-stuttgart/): Fill in the form to join a raffle for winning a PlayStation 5 - [Sales Representative – Key Accounts | Careers at BellSoft](https://bell-sw.com/jobs/sales-representative-key-accounts/): BellSoft is a major OpenJDK contributor providing Progressive Java Runtime and other tools for Java apps for modern architectures and cloud. - [Sales Representative – Key Accounts (Japan) | Careers at BellSoft](https://bell-sw.com/jobs/sales-representative-key-accounts-japan/): BellSoft is a major OpenJDK contributor providing Progressive Java Runtime and other tools for Java apps for modern architectures and cloud. - [Join the raffle and win a PlayStation 5 | BellSoft Java](https://bell-sw.com/kubecon/): Fill in the form to join a raffle for winning a PlayStation 5 - [Legal](https://bell-sw.com/legal-information/): - [Liberica Administration Center | BellSoft Java](https://bell-sw.com/liberica-administration-center/): LAC controls all Java runtimes in a network of any size. Automate the updates, find the security issues and check your licenses in a single convenient tool. - [Liberica Mission Control | BellSoft Java](https://bell-sw.com/liberica-mission-control/): Liberica Mission Control 7.1.1 is a low-overhead Java profiler built from OpenJDK JMC project. - [Liberica Native Image Kit | BellSoft Java](https://bell-sw.com/liberica-native-image-kit/): Liberica Native Image Kit (Liberica NIK) is an open source tool for accelerating JVM software. It converts Java bytecode into an optimized native executable. - [Liberica JDK End User License Agreement](https://bell-sw.com/liberica_eula/): - [Liberica NIK End User License Agreement](https://bell-sw.com/liberica_nik_eula/): - [Privacy Statement for BellSoft Software Subscriptions in Microsoft Azure](https://bell-sw.com/liberica_privacy_statement_azure/): - [Liberica JDK | Java runtime from an OpenJDK contributor](https://bell-sw.com/libericajdk/): Liberica JDK is a free and open-source implementation of the Java SE platform for modern Java deployments supported by a leading OpenJDK contributor. - [Liberica JDK container images | BellSoft Java](https://bell-sw.com/libericajdk-containers/): BellSoft provides ready Liberica JDK container images on Docker Hub, GHCR, and MCR. - [Liberica JDK for Embedded | BellSoft Java](https://bell-sw.com/libericajdk-for-embedded/): Discover the value of a Liberica JDK build made specifically for embedded devices: secure, reliable, cost-efficient, and open source runtime with enhanced performance. - [Liberica JDK Performance Edition | BellSoft Java](https://bell-sw.com/libericajdk-performance-edition/): Liberica JDK Performance Edition is a Java 11 runtime with all benefits of JDK 17: better startup time, throughput, latency of apps, and support with security patches. - [Liberica JDK with CRaC support | BellSoft Java](https://bell-sw.com/libericajdk-with-crac/): Liberica JDK with CRaC support allows you to save the optimal running state of your app and later instantly launch it in that state. - [BellSoft Media Kit | BellSoft Java](https://bell-sw.com/media-kit/): This is a media kit of BellSoft that can be distributed to members of the media. - [BellSoft Expands Alpaquita Linux with AArch64 Support for Arm-Based Containers and Cloud Java Workloads](https://bell-sw.com/news/bellsoft-adds-aarch64-support-to-alpaquita-linux-distribution-and-alpaquita-containers/): BellSoft’s Alpaquita Linux now includes AArch64 support, optimizing Java on Arm-based hardware for the cloud. Experience faster response, lower costs, and secure containerized Java applications with this powerful, lightweight Linux distribution tailored for modern workloads. - [BellSoft adds AArch64 support to Liberica JDK Performance Edition](https://bell-sw.com/news/bellsoft-adds-aarch64-support-to-liberica-jdk-performance-edition/): We are excited to announce the release of Liberica JDK Performance Edition for AArch64. This new edition integrates the powerful enhancements of JVM 17 into JDK 8 and 11, providing significant performance improvements for your Java workloads. - [BellSoft Announces Hardened Builder for Paketo Buildpacks™, Bringing Zero-CVE Container Images to Buildpacks® Users](https://bell-sw.com/news/bellsoft-announces-hardened-builder-for-paketo-buildpacks-bringing-zero-cve-container-images-to-buildpacks-users/): San Jose, California (July 21, 2026) BellSoft, a leading OpenJDK vendor, announces the general availability of a new hardened builder image for Paketo Buildpacks™. Built entirely on BellSoft Hardened Images, the builder gives Paketo Buildpacks™, users a direct path to improved security and compliance posture, including continuous vulnerability management under SLA, signed images, and Software Bill of Materials, all inherited automatically with no changes to existing developer workflows. - [BellSoft levels the performance of JDK 8 up to JVM 17 with Liberica JDK Performance Edition](https://bell-sw.com/news/bellsoft-levels-the-performance-of-jdk-8-up-to-jvm-17-with-liberica-jdk-performance-edition/): A widespread reason is the high financial and resource input required from companies for an upgrade to a newer LTS release. Meanwhile, the JVM in JDK 8 has lower performance, making your cloud use inefficient and therefore disappointing your customers. - [BellSoft releases Alpaquita Containers for Spring Boot](https://bell-sw.com/news/bellsoft-releases-alpaquita-containers-for-spring-boot/): Alpaquita containers are based on Alpaquita Linux and Liberica JDK Lite integration and enhance your app with better speed, higher security, and improved overall performance. - [BellSoft releases Alpaquita Containers with Coordinated Restore at Checkpoint support](https://bell-sw.com/news/bellsoft-releases-alpaquita-containers-with-coordinated-restore-at-checkpoint-support/): Java adaptation to the cloud environment comes with challenges, and the so-called "slow Java startup and warmup" is the one well-known. It can be described as a process that takes both time and memory for the traditional JVM to reach its exceptional peak performance in modern applications, resulting in higher costs and slower performance. - [BellSoft releases dedicated builds of Liberica JDK 17 and 21 with CRaC](https://bell-sw.com/news/bellsoft-releases-dedicated-builds-of-liberica-jdk-17-and-21-with-crac/): Spring is one of the most popular Java frameworks, thanks to its deep tech prospects, pre-announced adding CRaC support. This is some of the most anticipated news for Java developers. - [BellSoft releases Liberica JDK 21 for RISC-V with support](https://bell-sw.com/news/bellsoft-releases-liberica-jdk-21-for-risc-v-with-support/): RISC-V advantages are evident to many industries, from IoT to servers. Many companies in these industries use Java as their default programming language, and they are seeking to employ Java on RISC-V. - [BellSoft's Hardened Images Set New Standard for Container Security](https://bell-sw.com/news/bellsoft-s-hardened-images-set-new-standard-for-container-security/): San Jose, California (November 10, 2025) BellSoft, the OpenJDK vendor delivering the most complete Java experience, announces Hardened Images, a tool for enhancing the security and compliance of containerized applications in Kubernetes. BellSoft will be demonstrating Hardened Images at KubeCon 2025 in Atlanta, beginning today. - [BellSoft Upgrades Liberica JDK Performance Edition with JVM 21](https://bell-sw.com/news/bellsoft-upgrades-liberica-jdk-performance-edition-with-jvm-21/): BellSoft announces major upgrade to Liberica JDK Performance Edition, now powered by JVM 21. Delivers 5 to 10% better performance in most workloads, with gains of up to 40% in select cases. - [Container Security Issues Continue to Befuddle Software Developers](https://bell-sw.com/news/container-security-issues-continue-to-befuddle-software-developers/): A new survey from BellSoft found that the tools and strategies developers are using to protect their companies from container-related incidents are undermining security goals - [Spring Developers Have a Blindspot When It Comes to Container Security](https://bell-sw.com/news/spring-developers-have-a-blindspot-when-it-comes-to-container-security/): A survey from BellSoft found that Spring developers don’t know their Dockerfiles affect their security posture, aren’t using hardened images and can’t name their compliance framework, exposing their organizations, applications and users to considerable risk - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/2/): - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/3/): - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/4/): - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/): - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/news-coverage/2/): - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/news-coverage/3/): - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/news-coverage/): - [The BellSoft News | BellSoft Java](https://bell-sw.com/newsroom/press-releases/): - [Technical Support for Alpaquita Linux | Bellsoft](https://bell-sw.com/nik-support/): With the BellSoft support team, you can focus on deploying infrastructure or services with the utmost level of security in your containerized project. - [A comparative analysis of OpenJDK distributions | BellSoft Java](https://bell-sw.com/oracle-java-vs-open-jdk-distributions/): Compare key features of major OpenJDK distributions to find the best solution for your business - [Extended Support for Java 6 and Java 7 | Security Updates, Patches, and Extended Roadmap](https://bell-sw.com/pages/6-7-java-support/): Keep your legacy Java 6 and 7 applications secure with BellSoft's Liberica JDK. Benefit from extended support, and timely security updates through March 2028. Learn more about how we help you. - [OpenJDK Product Discovery API](https://bell-sw.com/pages/api/): - [IANA Updater Update Time Zones information in Liberica JDK/JRE](https://bell-sw.com/pages/iana-updater/): - [Liberica Native Image Kit Supported System Configurations | BellSoft Java](https://bell-sw.com/pages/nik-supported-configurations/): Discover Liberica Native Image Kit supported platforms. 100% open source tool for accelerating Java and JVM applications on Linux, Windows and macOS. - [Package Managers for Liberica](https://bell-sw.com/pages/package-managers/): - [BellSoft Liberica JDK Support | BellSoft Java](https://bell-sw.com/pages/support-login/): BellSoft provides High-Powered Support aimed to make the most of your OpenJDK investment. Rely on Java experts and take advantage of our safety guarantee. - [BellSoft Liberica JDK Support | BellSoft Java](https://bell-sw.com/pages/support-partner/): BellSoft provides High-Powered Support aimed to make the most of your OpenJDK investment. Rely on Java experts and take advantage of our safety guarantee. - [Supported System Configurations](https://bell-sw.com/pages/supported-configurations/): - [BellSoft Partner Program – Grow with Us!](https://bell-sw.com/partner-program/): Join the BellSoft Partner Program and grow your business with industry-leading Java and cloud-native solutions. Whether you resell, refer, or implement, we provide the tools, resources, and support to help you succeed. - [Privacy Policy](https://bell-sw.com/privacy-policy/): - [A guide to enhancing Java apps security | BellSoft Java](https://bell-sw.com/security-of-java-applications/): Get a grip on recommendations and techniques for increasing the security of Java apps - [Spring Container Security Report 2026 | BellSoft Survey](https://bell-sw.com/spring-container-security-2026/): 250 Spring developers surveyed at Spring I/O 2026. The findings reveal a security gap that surprised even us. Free report from BellSoft. - [Join the raffle and win a Steam Deck | BellSoft Java](https://bell-sw.com/spring-io/): Fill in the form to join a raffle for winning a PlayStation 5 - [State of Container Security 2025 | Bellsoft Java](https://bell-sw.com/state-of-container-security-2025/): Free report: 427 professionals reveal container security challenges. Learn why 49% can't keep up with maintenance and what solutions teams are requesting. Download now. - [Subscribe to our newsletter](https://bell-sw.com/subscribe/): Subscribe to our newsletter - [Terms of Use](https://bell-sw.com/terms-of-use/): - [Third-Party Licenses for BellSoft software](https://bell-sw.com/third_party_licenses/): - [BellSoft. Unified Java runtime for TCO reduction | BellSoft Java](https://bell-sw.com/unified-java-runtime/): A Unified Java Runtime aims to cover all your Java needs with a wide variety of Java tools for most system configurations and any purpose. - [5 Opinions on Distroless Images for Java Apps in 4 Minutes](https://bell-sw.com/videos/5-opinions-on-distroless-images-for-java-apps-in-4-minutes/): Distroless images, while smaller and aligned with modern immutable container practices, are not entirely "distroless" as they include a stripped-down Linux distribution, making them suitable for some, but not all, applications. - [5 Tips for Optimizing Java Performance on Kubernetes](https://bell-sw.com/videos/5-tips-for-optimizing-java-performance-on-kubernetes/): If your Java apps in the cloud struggle with high resource consumption, frequent container restarts, or slow response times, these five tips can help enhance their performance. First, set CPU and RAM limits properly based on load testing and account for Kubernetes overhead. - [5x Smaller Java Docker Images — 2025 Optimization Guide](https://bell-sw.com/videos/5x-smaller-java-docker-images-2025-optimization-guide/): In this video, I’ll show you how to make your Java Docker images 5 TIMES SMALLER. You’ll see the full process, from a heavy 587MB container to a lean 116MB image, ready for production. - [All 7 Java Garbage Collectors Explained](https://bell-sw.com/videos/all-7-java-garbage-collectors-explained/): In this complete guide to Java garbage collection, you will learn how the JVM memory model works, understand the differences between the Serial, Parallel, G1, ZGC, Shenandoah, CMS, and Epsilon collectors, and determine which garbage collector is best suited for your application's performance — from single-threaded programs to massive terabyte-scale heaps. - [An Overview of Java Garbage Collectors](https://bell-sw.com/videos/an-overview-of-java-garbage-collectors/): Java provides multiple garbage collectors (GCs) tailored to different performance needs. Serial GC is ideal for single-threaded apps but pauses all threads, while Parallel GC uses multiple threads to prioritize throughput. - [Async Profiler: Uncover Hidden Performance Issues in Java!](https://bell-sw.com/videos/async-profiler-uncover-hidden-performance-issues-in-java/): Let's discuss Async Profiler, the small and effective open-source low-overhead profiler for Hotspot JVM-based application. Itcan collect data on various JVM events like CPU usage, heap allocation, methods, and so on. It can monitor non-Java threads, native calls, and kernel functions. It is small, silent, and incredibly effective! - [Backend Developer Roadmap 2026: What You Need to Know](https://bell-sw.com/videos/backend-developer-roadmap-2026-what-you-need-to-know/): Backend complexity keeps growing, and frameworks can't keep up. In 2026, knowing React or Django isn't enough. You need fundamentals that hold up when systems break, traffic spikes, or your architecture gets rewritten for the third time.I've been building production systems for 15 years. This roadmap covers three areas that separate people who know frameworks from people who can actually architect backend systems: data, architecture, and infrastructure. This is about how to think, not what tools to install. - [Best Oracle Java Alternatives in 2026 Comparison of OpenJDK Distributions](https://bell-sw.com/videos/best-oracle-java-alternatives-in-2026-comparison-of-openjdk-distributions/): A comparison of major OpenJDK distributions (Temurin, Liberica, Zulu, Corretto, Semeru, etc.), covering who maintains them, how updates are delivered, and what lifecycle guarantees they provide. We also explain why upstream OpenJDK isn’t production-ready and how your vendor choice impacts real-world systems. Useful for Spring Boot, containers, and Kubernetes to avoid hidden risks and choose the right runtime. - [Boost The Performance and Security of Your Spring Boot App with Alpaquita Containers](https://bell-sw.com/videos/boost-the-performance-and-security-of-your-spring-boot-app-with-alpaquita-containers/): Alpaquita Containers offer a secure, high-performance solution for running Spring Boot applications in the cloud. These lightweight containers, built on Liberica JDK Lite and Alpaquita Linux, optimize memory and disk usage, reducing resource consumption by up to 30%. - [Build RAG System with Spring AI: No More AI Lies](https://bell-sw.com/videos/build-rag-system-with-spring-ai-no-more-ai-lies/): Struggling to find answers in your own documentation? Your LLM is too. Hallucinations happen because models don’t know your data. RAG (Retrieval Augmented Generation) turns unstructured docs into searchable vector embeddings, so your LLM retrieves facts instead of inventing them. This tutorial walks through the full RAG workflow in Spring AI: document ingestion with TikaDocumentReader, embedding generation, vector storage (pgvector, Choma, Milvus, Oracle), and similarity-based retrieval. You’ll build two endpoints: one for uploading documents and one for answering questions strictly using your indexed data. When the system doesn’t know, it says so—no more confident nonsense. Designed for Java teams bringing AI into production systems where accuracy matters. You’ll learn the pattern, get the code, and deploy an LLM that finally stops hallucinating. - [Build Typed AI Agents in Java with Embabel](https://bell-sw.com/videos/build-typed-ai-agents-in-java-with-embabel/): Most Java AI demos stop at prompt loops. That doesn't scale in production. In this video, we integrate Embabel into an existing Spring Boot application and build a multi-step, goal-driven agent for incident triage. Instead of manually orchestrating prompt → tool → prompt cycles, we define typed actions and let the agent plan across deterministic and LLM-powered steps. We parse structured input with Ollama, query MongoDB deterministically, classify risk using explicit thresholds, rank affected implants, generate a constrained root cause hypothesis, and produce a bounded containment plan. LLM handles reasoning. Java enforces rules. This is about controlled AI workflows on the JVM — not prompt glue code. - [Buildpacks for Spring Boot](https://bell-sw.com/videos/buildpacks-for-spring-boot/): Buildpacks for Spring Boot: no Dockerfiles, no hassle — just production-ready container images in one command. Tired of maintaining Dockerfiles? In this tutorial, you’ll learn how to use buildpacks to create optimized Spring Boot containers — fast, secure, and cloud-ready — with just one command. We’ll show what happens under the hood: automatic dependency detection, layered image creation, memory tuning, SBOM generation, and how to tweak builds with just a few plugin options. Need faster startup, smaller image size, or JFR monitoring? Buildpacks can handle it — and we’ll show you how. - [Docker Container Image Security: 13 Best Practices](https://bell-sw.com/videos/docker-container-image-security-13-best-practices/): This video presents 13 practical recommendations for reducing your attack surface and detecting malicious activity more quickly. You’ll learn how to create simple, immutable, and deterministic images using multi-stage builds, distroless bases, and non-root users. We cover SBOM generation with Syft, provenance verification with Cosign, CVE scanning workflows, and secret management strategies. From choosing LTS base images like Alpaquita Linux to implementing host-level protections, these practices will help you confidently deliver secure containers. It’s ideal for Java developers, DevOps engineers, and architects building production-grade infrastructure. - [Dockerize Spring Boot Wisely: 6 tips to improve the container images of your Spring Boot apps](https://bell-sw.com/videos/dockerize-spring-boot-wisely-6-tips-to-improve-the-container-images-of-your-spring-boot-apps/): Your Spring Boot applications deserve a top-notch package! - [Downgraded Java to JDK 1.1 After 30 Years… (part 1)](https://bell-sw.com/videos/downgraded-java-to-jdk-1-1-after-30-years-part-1/): How should we change Java 23 code for it to run on Java 1.1? We go line by line, removing modern features like records, sealed classes, switch expressions, var, and more. Each step reveals what breaks, how to rewrite it, and what you lose in the process. If you've ever wondered how far modern Java has drifted from its roots - this is your deep dive into that gap. This is Part 1 of the Java Downgrade Challenge, where we descend version by version until we reach Java 8. Subscribe to our channel to find out how we go even deeper - all the way down to Java 1.1. Stay tuned! - [Dynamic SQL Queries with Spring Data JPA in 6 Minutes](https://bell-sw.com/videos/dynamic-sql-queries-with-spring-data-jpa-in-6-minutes/): If your repository layer has multiple queries for different filter combinations, your data access logic is already getting harder to maintain. In this video, we implement dynamic SQL queries in Spring Data JPA using Specifications — a composable approach that helps avoid query duplication and keeps your filtering logic clean. We build a flexible filtering system with optional parameters (category, language, format, price) and show how Specification.unrestricted() skips empty filters, while Specification.allOf(...) combines them into a single query. We also address a common issue: string-based field access. It’s fragile and can break at runtime when your model changes. Using the JPA Static Metamodel, we move to compile-time safety. The result is a cleaner, more maintainable way to implement dynamic filtering in Spring-based applications. - [Flyway in Spring Boot: Step-by-Step tutorial with Maven](https://bell-sw.com/videos/flyway-in-spring-boot-step-by-step-tutorial-with-maven/): Learn how to use Flyway in Spring Boot with Maven for smooth and reliable database migrations. In this hands-on tutorial, we cover everything from setting up PostgreSQL in Docker, configuring Flyway in your application, writing versioned and repeatable migrations, to using Flyway in CI/CD pipelines with GitHub Actions. Whether you’re new to Flyway or want to master schema version control in Spring Boot, this video will guide you step by step. - [GraalVM for Java Developers: The Ultimate Beginner’s Guide](https://bell-sw.com/videos/graalvm-for-java-developers-the-ultimate-beginner-s-guide/): What is GraalVM and how can it improve your Java applications? In just 10 minutes, this video explains the three main components of GraalVM — the JIT compiler, Native Image, and Polyglot API. Learn how to boost performance, reduce startup time, and combine multiple languages in one app. Whether you’re building microservices, serverless apps, or just exploring modern JVM tooling, this is your quick-start guide to GraalVM. - [Hardened Container Images 101: What, Why, and How for DevSecOps [2025]](https://bell-sw.com/videos/hardened-container-images-101-what-why-and-how-for-devsecops-2025/): Why do production containers still ship with 600+ CVEs, package managers, and compilers? Most teams inherit bloated base images without ever checking what’s inside. Hardened container images fix this by removing unnecessary tools, enforcing immutability, and providing verifiable provenance. This tutorial breaks down the four pillars of hardened images—minimal base layers, low-to-zero CVEs, immutable runtimes, and SBOM-backed provenance—and shows how they compare to traditional and distroless images. You’ll also see practical examples: migrating from OpenJDK to hardened Liberica images with multi-stage builds, pinning images by digest, verifying signatures with Cosign, and integrating these checks into a production-ready CI/CD pipeline. Perfect for Java developers, DevOps engineers, and architects who want compliance-ready containers and dramatically fewer CVEs—without reinventing security every sprint. - [Hibernate: Ditch or Double Down? When ORM Isn't Enough](https://bell-sw.com/videos/hibernate-ditch-or-double-down-when-orm-isn-t-enough/): Every Java team debates Hibernate at some point: productivity champion or performance liability? Both are right. This video shows you when to rely on Hibernate's ORM magic and when to drop down to SQL. We walk through production scenarios: domain models with many-to-many relations where Hibernate excels, analytical reports with window functions where JDBC dominates, and hybrid architectures that use both in the same Spring Boot codebase. You'll see real code examples: the N+1 query trap that kills performance, complex window functions and anti-joins that Hibernate can't handle, equals/hashCode pitfalls with lazy loading, and practical two-level caching strategies. We also explore how Hibernate works under the hood—translating HQL to database-specific SQL dialects, managing sessions and transactions through JDBC, implementing JPA specifications. The strategic insight: modern applications need both ORM convenience for transactional business logic and SQL precision for data-intensive analytics. Use Hibernate for CRUD and relationship management. Use SQL where ORM abstractions leak or performance demands direct control. - [How to Create Dynamic SQL Queries with Spring Boot](https://bell-sw.com/videos/how-to-create-dynamic-sql-queries-with-spring-boot/): Build SQL queries dynamically based on the user input with the Specification interface. Use the JPA Static Model Generator to create type-safe queries. Run the tests to check your application. - [How to Improve the Performance of Legacy Java Code](https://bell-sw.com/videos/how-to-improve-the-performance-of-legacy-java-code/): Your application is running on Java 8 or 11 and you can't migrate to a newer JDK ? Let's discuss the techniques that can speed up your Java app, cut memory usage, and reduce garbage collection pauses — and let you stay (for now) on the current JDK version. - [How to install Liberica Native Image Kit on Windows PC](https://bell-sw.com/videos/how-to-install-liberica-native-image-kit-on-windows-pc/): Liberica Native Image Kit is a multilingual GraalVM-based set of utilities for creating native images. This guide will help you to install it on Windows PC. - [How to Profile Java Applications in Docker Containers with JFR](https://bell-sw.com/videos/how-to-profile-java-applications-in-docker-containers-with-jfr/): Java applications in Docker containers using Java Flight Recorder (JFR), a built-in OpenJDK tool. It covers three profiling methods: enabling JFR at application startup, attaching to a running container using an ephemeral container with jcmd, and monitoring real-time performance with JDK Mission Control via remote JVM connections. - [How to use AppCDS with Spring Boot](https://bell-sw.com/videos/how-to-use-appcds-with-spring-boot/): This tutorial demonstrates how to use Application Class Data Sharing (AppCDS) and Ahead-of-Time (AOT) processing with Spring Boot applications to reduce startup time by 40–50%. AppCDS creates an archive of parsed classes for faster loading, requiring no code changes, and works both locally and in containers. The tutorial covers building optimized Docker images using Dockerfiles or Buildpacks for efficient deployment and improved performance. - [How to use CRaC with Spring Boot in a Docker Container](https://bell-sw.com/videos/how-to-use-crac-with-spring-boot-in-a-docker-container/): CRaC (Coordinated Restore at Checkpoint) is an OpenJDK project designed to significantly reduce startup and warmup times of Java applications to milliseconds. This tutorial demonstrates using CRaC with a Spring Boot application running in a Docker container, specifically the Spring Boot Petclinic app (version 3.2 or later). - [I Solved Advent of Code 2025 in Kotlin: Here's How It Went](https://bell-sw.com/videos/i-solved-advent-of-code-2025-in-kotlin-here-s-how-it-went/): Every year, Advent of Code spawns thousands of solutions — but few engineers step back to see the bigger picture. This is a complete walkthrough of all 12 days from 2025, focused on engineering patterns rather than puzzle statements. We cover scalable techniques: interval math without brute force, dynamic programming, graph algorithms (JGraphT), geometry with Java AWT Polygon, and optimization problems that need constraint solvers like ojAlgo. You'll see how Java and Kotlin handle real constraints, how visualizations validate assumptions, and when to reach for libraries instead of writing everything from scratch. If you love puzzles, programming—or both—and maybe want to learn how to solve them on the JVM, this is for you. - [Java 25 LTS: The New Features in JDK 25](https://bell-sw.com/videos/java-25-lts-the-new-features-in-jdk-25/): In this video, we go through every single JEP in JDK 25, explaining what it does, why it matters, and how it impacts real-world development. Whether you’re a Java veteran or just getting started, this breakdown will help you stay ahead of the curve. - [Java 26 Preview: New JEPs and What They Mean for You](https://bell-sw.com/videos/java-26-preview-new-jeps-and-what-they-mean-for-you/): Java 26 is the next feature release that brings features for enhanced performance, security, and developer experience. This video discusses the upcoming JDK 26 release, highlighting ten JEPs including JEP 500. JEP 500 focuses on preparing developers for future restrictions on mutating final fields in Java, emphasizing their role in maintaining immutable state. This is crucial for robust programming and understanding the nuances of mutable vs immutable data, especially concerning an immutable class in java. We also touch upon the broader implications for functional programming in Java. - [Java Developer Roadmap 2026: From Basics to Production](https://bell-sw.com/videos/java-developer-roadmap-2026-from-basics-to-production/): Most Java roadmaps teach tools. This one teaches order — the only thing that actually gets you to production. You don’t need to learn everything. You need to learn the right things, in the right sequence. In this video, we break down a practical Java developer roadmap for 2026 — from syntax and OOP to Spring, databases, testing, and deployment. Structured into 8 levels, it shows how real engineers grow from fundamentals to production-ready systems. We cover what to learn and what to ignore: core Java, collections, streams, build tools, Git, SQL and JDBC before Hibernate, the Spring ecosystem, testing with JUnit, and deployment with Docker and CI/CD. You’ll also understand why most developers get stuck — jumping into frameworks too early, skipping SQL, or treating tools as knowledge. This roadmap gives you a clear path into real-world Java development — with priorities, trade-offs, and production context. - [Java Downgrade Challenge: From JDK 8 to 1.1 (Part 2)](https://bell-sw.com/videos/java-downgrade-challenge-from-jdk-8-to-1-1-part-2/): In Part 2 of the Java Downgrade Challenge, we continue our journey — now from Java 8 all the way to Java 1.1. No streams, no lambdas, no generics, no collections — and at one point, we even boot up Windows 98. If you thought Part 1 was painful, this one unwinds Java history line by line. By the end, the familiar Java from today will be almost gone. - [Java DTO Guide: Fix Your API Design with One Simple Pattern](https://bell-sw.com/videos/java-dto-guide-fix-your-api-design-with-one-simple-pattern/): This tutorial shows how to use the Data Transfer Object (DTO) pattern to transfer data between application layers. We use Java records to reduce boilerplate code and the MapStruct library that simplifies Java bean mapping. - [Java Flight Recorder Tutorial: How to Profile Java Applications](https://bell-sw.com/videos/java-flight-recorder-tutorial-how-to-profile-java-applications/): High CPU, GC spikes, or slow startup are common production issues, but logs and metrics don’t always reveal what the JVM is actually doing. Java Flight Recorder (JFR) provides a precise, low-overhead view of JVM behavior, safe for use even in production environments. In this video, you’ll learn how to use JFR to identify real bottlenecks such as CPU hotspots, memory allocation pressure, thread contention, and I/O stalls. We walk through the full workflow, including starting recordings with JVM flags, controlling them via jcmd, running JFR inside Docker containers, and attaching to live systems using ephemeral containers. Then we analyze a real Spring Boot recording in JDK Mission Control, breaking down GC behavior, allocation patterns, thread states, and method-level hotspots. If you want to move from symptoms to root cause with more confidence, this approach will help. Full article with commands and examples: [https://bell-sw.com/blog/how-to-profile-java-applications-with-jfr-beginner-s-guide/](https://bell-sw.com/blog/how-to-profile-java-applications-with-jfr-beginner-s-guide/) - [Java in 2025: Busting the Biggest Myths](https://bell-sw.com/videos/java-in-2025-busting-the-biggest-myths/): Think Java is slow, outdated, or only for old-school enterprise apps? Think again. In this episode of Java Myth Busters, we debunk six common myths about Java, including performance issues, security concerns, and the myth that Java is dead. Discover how the JVM, Spring Boot, GraalVM, and modern JDK updates make Java one of the most powerful, scalable, and relevant languages in 2025. - [Java in 2025: LTS Release, AI on JVM, Framework Modernization](https://bell-sw.com/videos/java-in-2025-lts-release-ai-on-jvm-framework-modernization/): Java in 2025 isn't about headline features, it's about how production systems changed under the hood. While release notes focus on individual JEPs, the real story is how the platform, frameworks, and tooling evolved to improve stability, performance, and long-term maintainability. In this video, we look at Java from a production perspective. What does Java 25 LTS mean for teams planning to upgrade? How are memory efficiency, startup time, and observability getting better? Why do changes like Scoped Values and AOT optimizations matter beyond benchmarks? We also cover the broader ecosystem: Spring Boot 4 and Framework 7, AI on the JVM with Spring AI and LangChain4j, Kotlin's growing role in backend systems, and tooling updates that make upgrades easier. Finally, we touch on container hardening and why runtime and supply-chain decisions matter just as much as language features. - [Java Memory Options You Need in Production](https://bell-sw.com/videos/java-memory-options-you-need-in-production/): JVM memory tuning can be tricky. Teams increase -Xmx and assume the problem is solved. Then the app still hits OOM. Because maximum heap size is not the only thing that affects memory footprint. The JVM uses RAM for much more than heap: metaspace, thread stacks, JIT/code cache, direct buffers, and native allocations. That’s why your process can run out of memory while heap still looks “fine”. In this video, we break down how JVM memory actually works and how to control it with a minimal, production-safe set of flags. We cover heap sizing (-Xms, -Xmx), dynamic resizing, direct memory (-XX:MaxDirectMemorySize), and total RAM limits (-XX:MaxRAMPercentage) — especially in containerized environments like Docker and Kubernetes. We also explain GC choices such as G1, ZGC, and Shenandoah, when defaults are enough, and why GC logging (-Xlog:gc*) is mandatory before tuning. Finally, we show how to diagnose failures with heap dumps and OOM hooks. This is not about adding more flags. It’s about understanding what actually consumes memory — and making decisions you can justify in production. - [JavaFX and scene Builder: Create UIs with a few clicks](https://bell-sw.com/videos/javafx-and-scene-builder-create-uis-with-a-few-clicks/): Creating user interfaces with JavaFX is easy! Simply install Scene Builder and create a layout with a few clicks! In this video we will create an interactive To Do app and in the process we will learn how to design UIs without coding. - [JDBC Connection Pools in Microservices. Why They Break Down (and What to Do Instead)](https://bell-sw.com/videos/jdbc-connection-pools-in-microservices-why-they-break-down-and-what-to-do-instead/): In this livestream, Catherine is joined by Rogerio Robetti, the founder of Open J Proxy, to discuss why traditional JDBC connection pools break down when teams migrate to microservices, and what is a more efficient and reliable approach to organizing database access with microservice architecture. - [JDBC vs ORM vs jOOQ: Choose the Right Java Database Tool](https://bell-sw.com/videos/jdbc-vs-orm-vs-jooq-choose-the-right-java-database-tool/): Still unsure what is the difference between JPA, Hibernate, JDBC, or jOOQ and when to use which? This video clarifies the entire Java database access stack with real, production-oriented examples. We start at the foundation, which is JDBC, a low-level API every other tool eventually relies on for database communication. Then, we go through the ORM concept, JPA as a specification of ORM, Hibernate as the implementation and extension of JPA, and Blaze Persistence as a powerful upgrade to JPA Criteria API. From there, we take a different path with jOOQ: a database-first, SQL-centric approach that provides type-safe queries and catches many SQL errors at compile time instead of runtime. You’ll see when raw JDBC makes sense for small, focused services, when Hibernate fits CRUD-heavy domains, and when jOOQ excels at complex reporting and analytics. We discuss real performance pitfalls such as N+1 queries and lazy loading, and show practical combination strategies like “JPA for CRUD, jOOQ for reports.” The goal is to equip you with clarity so that you can make informed architectural decisions based on domain complexity, query patterns, and long-term maintainability. - [JDK 24: The New Features in Java 24](https://bell-sw.com/videos/jdk-24-the-new-features-in-java-24/): JDK 24 is in Rampdown Phase One, which means, we know all the JEPs targeted to this release. And there are a lot of them, so it is time to discuss this new Java release! - [JEP 483: Ahead-of-Time Class Loading & Linking. Project Leyden in JDK 24](https://bell-sw.com/videos/jep-483-ahead-of-time-class-loading-linking-project-leyden-in-jdk-24/): JEP 483 introduces Ahead-of-Time (AOT) Class Loading and Linking in JDK 24, which enhances Java application startup times by loading and linking classes ahead of time and storing them in a reusable AOT cache. This feature, part of Project Leyden, reduces the JVM's workload during startup without requiring changes to application code, though a training run mimicking production is needed to create an efficient cache. Early tests with a Spring Boot app showed significant improvements, cutting startup time from two seconds to just one second. - [jOOQ Deep Dive: CTE, MULTISET, and SQL Pipelines](https://bell-sw.com/videos/jooq-deep-dive-cte-multiset-and-sql-pipelines/): Some backend developers reach the point where the ORM stops being helpful. Complex joins, nested result graphs, or CTE pipelines quickly push frameworks like Hibernate to their limits. And when that happens, teams often end up writing fragile raw SQL strings or fighting performance issues like the classic N+1 query problem. In this video, we build a healthcare scheduling application NeonCare using jOOQ, Spring Boot 4, and PostgreSQL, and show how to write production-grade SQL directly in Java while keeping full compile-time type safety. - [JRush | Container Essentials: Fast Builds, Secure Images, Zero Vulnerabilities](https://bell-sw.com/videos/jrush-container-essentials-fast-builds-secure-images-zero-vulnerabilities/): Web-conference for Java developers focused on hands-on strategies for building high-performance containers, eliminating CVEs, and detecting security issues before production. - [JRush Ep 5 - Advanced Security Strategies for Java and Spring Applications](https://bell-sw.com/videos/jrush-ep-5-advanced-security-strategies-for-java-and-spring-applications/): Web-conference for engineers focused on real-world solutions for enhancing Java security, managing AI threats, and learning about zero-day responses. - [JRush Episode 4: Q&A Session](https://bell-sw.com/videos/jrush-episode-4-q-a-session/): JRush Episode 4 “Modern Java development for Banking and FinTech”, we had a very fruitful discussion with our speakers, Dr Mo Haghighi, Mary Grygleski, and Dmitry Chuyko - [Jrush episode 4th: Build your Cloud Native Application with Kubernetes](https://bell-sw.com/videos/jrush-episode-4th-build-your-cloud-native-application-with-kubernetes/): Learn how to build your Cloud Native Application with Dr Mo Haghighi - [JRush episode 4th: Event streaming with Apache Pulsar](https://bell-sw.com/videos/jrush-episode-4th-event-streaming-with-apache-pulsar/): Discover how to perform efficient and reliable event streaming with Apache Pulsar with Mary Grygleski, Streaming Developer Advocate at DataStax - [JRush episode 4th: Fresh Java on modern Arm servers](https://bell-sw.com/videos/jrush-episode-4th-fresh-java-on-modern-arm-servers/): Explore the world of modern Arm servers with Dmitry Chuyko, Senior Performance Architect at BellSoft. - [Master Java Profiling in 2025: Tools, Techniques, and Real-World Tips](https://bell-sw.com/videos/master-java-profiling-in-2025-tools-techniques-and-real-world-tips/): In this complete guide to Java profiling, you will learn sampling and instrumentation techniques, compare the 7 best tools (JFR, VisualVM, Async Profiler, JProfiler, YourKit, Digma.ai, New Relic), and master how to detect memory leaks and analyze CPU usage. - [Master Java Profiling: Tools, Techniques, and Real-World Tips](https://bell-sw.com/videos/master-java-profiling-tools-techniques-and-real-world-tips/): Java profiling allows to rapidly identify and fix performance bottlenecks in your program. In this video we explain what is profiling, introduce popular profiling tools, list their pros and cons, and provide useful tips and code examples. - [OpenJDK projects that we anticipate](https://bell-sw.com/videos/openjdk-projects-that-we-anticipate/): OpenJDK is actively evolving, with projects like Leyden, Valhalla, Babylon, and Lilliput aiming to enhance Java's performance and capabilities. Leyden focuses on faster startup and warmup by reusing precompiled code, while Valhalla introduces value objects, primitive classes, and specialized generics for better memory and runtime efficiency. - [Advanced Kotlin Techniques for Spring Developers](https://bell-sw.com/videos/pasha-finkelshteyn-at-spring-i-o-2024-advanced-kotlin-techniques-for-spring-developers/): As a seasoned developer, you're likely already familiar with Spring. But Kotlin can take your developer experience with Spring to the next level! Join this session and learn how to add new functionality to existing classes with Kotlin extension functions, use Kotlin bean definition DSL, improve third-party libraries with varargs, and leverage coroutines with Spring idiomatically. By the end of this talk, you'll have a deeper understanding of the advanced Kotlin techniques that are available to you as a Spring developer and be able to use them effectively in your projects. - [PF4J: Plugin Framework for Java. Plugin Systems for Backend](https://bell-sw.com/videos/pf4j-plugin-framework-for-java-plugin-systems-for-backend/): PF4J (Plugin Framework for Java) is a lightweight framework that allows developers to create modular applications using plugins. It enables the integration of custom code into applications through extension points, with support for lifecycle management and Spring integration for dependency injection. PF4J is useful for both desktop and web applications, offering flexibility in scaling and extending functionality without altering the core system. - [Reducing Java Startup Time: 4 Approaches](https://bell-sw.com/videos/reducing-java-startup-time-4-approaches/): Java application startup can be significantly accelerated using modern tools. AppCDS stores preloaded classes in a shared archive, cutting startup time by up to 50%, while Project Leyden shifts optimizations to earlier stages with ahead-of-time compilation. GraalVM Native Image creates standalone executables for sub-second startup, and CRaC restores pre-warmed application states for instant readiness. - [SBOMs & Java Security: Stay Compliant, Stay Protected!](https://bell-sw.com/videos/sboms-java-security-stay-compliant-stay-protected/): A software bill of materials or SBOM is crucial for the modern development environment full of regulations, vulnerabilities, and hidden threats. In this video, we discuss what SBOMs are and how they help dealing with compliance audits and application security. We will explore the best open-source tools for generating SBOMs and provide a tutorial on creating an SBOM for your project and analyzing it for vulnerabilities. - [Should You Still Use LOMBOK in 2025?](https://bell-sw.com/videos/should-you-still-use-lombok-in-2025/): Have you ever wondered how @Data, @Builder, and @Getter actually work? Lombok doesn’t just generate code — it rewrites your AST during compilation using internal, unofficial APIs. And while many developers enjoy the reduced boilerplate, few realize the hidden risks behind it. - [Sizing JDBC Connection Pools for Real Production Load](https://bell-sw.com/videos/sizing-jdbc-connection-pools-for-real-production-load/): Many production outages start with connection pool exhaustion. Your app waits seconds for connections while queries take milliseconds; yet, most teams run default settings that collapse under load. This video shows how to configure connection pools that survive real production traffic: sizing based on database limits and thread counts, setting timeouts that prevent cascading failures, and implementing an open source database proxy Open J Proxy for centralized connection management with virtual connection handles, client-side load balancing, and slow query segregation. For senior Java developers, DevOps engineers, and architects who need database performance that holds under pressure. - [Spring AI: Streaming LLM Tokens with NDJSON in Spring Boot](https://bell-sw.com/videos/spring-ai-streaming-llm-tokens-with-ndjson-in-spring-boot/): Streaming LLM responses smoothly is harder than it looks. SSE often breaks LLM token spacing and words merge. This video shows how to fix it using Spring AI, WebFlux, and NDJSON. You’ll learn why Server-Sent Events can fail with modern LLM tokenizers, how NDJSON provides reliable token streaming, and how to build a production-ready pipeline with proper error handling and backpressure control. We cover configuring Spring AI ChatClient with Ollama, creating a reactive NDJSON endpoint, handling errors with onErrorResume, managing backpressure with limitRate, and consuming the NDJSON stream on a Vaadin frontend using WebClient. The result: stable, clean LLM streaming. Tested with Spring Boot 3.5.7, Java 25 (Liberica JDK), and Ollama both locally and in Docker. - [Spring Boot Buildpacks: Optimize, Secure, and Shrink Your Containers!](https://bell-sw.com/videos/spring-boot-buildpacks-optimize-secure-and-shrink-your-containers/): Buildpacks let you containerize Spring Boot apps without writing Dockerfiles, but the default settings aren’t always optimal. In this video, I’ll show you how to fine-tune buildpacks for faster startup, smaller images, and better security—so you get the best performance with minimal effort. - [Spring Data MongoDB: From Repositories to Aggregations](https://bell-sw.com/videos/spring-data-mongodb-from-repositories-to-aggregations/): Spring Data MongoDB breaks down fast once CRUD meets production—real queries, actual data volumes, analytics. What looks simple at first quickly turns into unreadable repository methods, overfetching, and slow queries. In this video, I walk through building a production-style Spring Boot application using Spring Data MongoDB — starting with basic setup and repositories, then moving into indexing, projections, custom queries, and aggregation pipelines. You'll see how MongoDB's document model changes data design compared to SQL, when embedding helps, and when it becomes a liability. We cover where repository method naming stops scaling, how to use @Query safely, when to switch to MongoTemplate, and how to reduce payload size with projections and DTOs. Finally, we implement real MongoDB aggregations to calculate analytics directly in the database and test everything against a real MongoDB instance using Testcontainers. This is not another MongoDB overview. It's a practical guide to actually using Spring Data MongoDB in production without fighting the database. - [Spring Developer Roadmap 2026: What You Need to Know](https://bell-sw.com/videos/spring-developer-roadmap-2026-what-you-need-to-know/): Spring Boot is powerful. But knowing the framework isn’t the same as understanding backend engineering. In this video, I walk through the roadmap I believe matters for a Spring developer in 2026. We start with data. That means real SQL — CTEs, window functions, normalization trade-offs — and understanding what ACID and BASE actually imply for system guarantees. Spring Data JPA is useful, but you still need to know what happens underneath. Then architecture: microservices vs modular monolith, serverless, CQRS, and when HTTP, gRPC, Kafka, or WebSockets make sense. Not as buzzwords — but as design choices with trade-offs. Security and infrastructure follow: OWASP Top 10, AuthN vs AuthZ, encryption in transit and at rest, Docker, Kubernetes, Infrastructure as Code, and observability with Micrometer, OpenTelemetry, and Grafana. This roadmap isn’t about mastering every tool. It’s about knowing what affects reliability in production. - [Stop Using DTOs – A Cleaner Way for Your Java APIs](https://bell-sw.com/videos/stop-using-dtos-a-cleaner-way-for-your-java-apis/): Still creating DTOs for every API in your Spring Boot project? You might be overcomplicating things. In this video, we show why DTOs aren’t always necessary and how to replace them with @JsonIgnore, @JsonView, and Jackson Mixins. You’ll see real examples of hiding sensitive fields, creating role-based views, and cutting boilerplate — all while keeping your API safe, clean, and easy to maintain. - [Swagger UI Integration in Spring Boot 3](https://bell-sw.com/videos/swagger-ui-integration-in-spring-boot-3/): How to integrate Swagger UI in Spring Boot 3 using springdoc-openapi This beginner-friendly tutorial shows how to document REST APIs in Java using Swagger UI, customize OpenAPI metadata, group and version endpoints, and secure the Swagger interface — all with Spring Boot 3 and Java 21. - [TOP-5 Lightweight Linux Distributions for Containers](https://bell-sw.com/videos/top-5-lightweight-linux-distributions-for-containers/): In this video, we compare five lightweight Linux distributions commonly used as base images: Alpine, Alpaquita, Chiseled Ubuntu, RHEL UBI Micro, and Wolfi. There are no rankings or recommendations — just a structured look at how these distros differ so you can evaluate them in your own context. - [Top 7 JavaFX Testing Mistakes You Need To Avoid!](https://bell-sw.com/videos/top-7-javafx-testing-mistakes-you-need-to-avoid/): Stop making these common JavaFX testing mistakes! No more random NullPointerExceptions or deadlocks — in this video, we’ll show you how to fix the 7 most frequent TestFX issues when testing JavaFX applications. Learn how to work with FX threads, integrate with Spring Boot, avoid event-queue race conditions, handle pixel-level test differences, set up headless continuous integration with Monocle, and properly separate business logic from UI tests. - [Spring Boot to Native Image in 1 Command — GraalVM Tutorial](https://bell-sw.com/videos/turn-your-4-second-java-startup-into-0-5-seconds-with-graalvm-native-image/): This video covers theory and hands-on practice: how Native Image works, distribution comparison (Liberica NIK, Oracle GraalVM, Mandrel), and real Spring Boot applications including an AI chatbot and Vaadin frontend. From Maven plugins to Docker deployment and tracing agent troubleshooting. - [Vaadin Tutorial: From Spring Boot to Beautiful UI Fast](https://bell-sw.com/videos/vaadin-tutorial-from-spring-boot-to-beautiful-ui-fast/): In this guide, I’ll show you how to build a fully functional Java application with authentication, data tables, filters, and a custom cyberpunk theme using Vaadin. - [Why Test Performance When Scaling K8s](https://bell-sw.com/videos/why-test-performance-when-scaling-k8s/): Running the HotSpot JVM in containers requires careful tuning of resource limits, garbage collection, and JVM options to maximize performance and avoid issues like container restarts or poor scaling. - [Will AI Replace Developers? A Vibe Coding Reality Check 2025](https://bell-sw.com/videos/will-ai-replace-developers-a-vibe-coding-reality-check-2025/): Can AI replace software engineers? ChatGPT, Copilot, and LLM-powered vibe coding tools promise to automate development—but after testing them against 17 years of production experience, the answer is more nuanced than the hype suggests. Full project generation produces over-engineered code that's hard to refactor. AI assistants excel at boilerplate but fail at business logic. MCP servers solve hallucination problems but create context overload. Meanwhile, DevOps automation actually works. This breakdown separates AI capabilities from marketing promises—essential for teams integrating LLMs and copilots without compromising code quality or architectural decisions. - [Liberica JDK | Java runtime from an OpenJDK contributor](https://bell-sw.com/vulnerability-report/): Liberica JDK is a free and open-source implementation of the Java SE platform for modern Java deployments supported by a leading OpenJDK contributor. - [It's Fine Actually: Doing Better in Legacy Java](https://bell-sw.com/webinars/java8-fine-actually/): Still on Java 8 or 11? No judgment, most of us are. Maybe the rewrite isn't coming. Maybe the upgrade to Java 17 or 21 is always six months away. Maybe that old system just… works. This webinar is about what to do when you're not moving on. Because staying doesn't have to mean suffering, and it definitely doesn't mean giving up on good engineering. - [Mastering Kubernetes: Best Practices for Deploying Spring Boot Applications | Java runtime from an OpenJDK contributor](https://bell-sw.com/webinars/mastering-kubernetes-best-practices/): - [Reactive Web Apps With 100% Java With Vaadin](https://bell-sw.com/webinars/reactive-web-apps-with-vaadin/): Full-stack development can be intimidating, especially when going beyond the typical CRUD. A reactive application with real-time updates requires knowing things like WebSockets on top of the regular frontend stuff. But what if you could build a full-featured full-stack web app using only straightforward Java? No JavaScript, no HTML, no CSS. Just Java. Join this live coding session to discover how to develop a reactive web app using Spring Boot and the open-source Vaadin framework. Starting from scratch, we'll cover everything: real-time updates, external services, and a good-looking UI. All in Java. - [掌握 Kubernetes: 部署 Spring Boot 应用程序的最佳实操 | Java runtime from an OpenJDK contributor](https://bell-sw.com/zh-cn/webinars/mastering-kubernetes-best-practices/): ## Downloads - [Download Alpaquita Linux | BellSoft Java](https://bell-sw.com/pages/downloads/alpaquita/): Download Alpaquita Linux, a free and open-source Linux distro tailored to Java apps and microservices. - [Access Alpaquita Linux cloud images for AWS | BellSoft](https://bell-sw.com/pages/downloads/alpaquita-cloud-image/): Access Alpaquita Linux, the Alpine-based build fine-tuned for Java with glibc and musl support. - [Access Alpaquita Linux cloud images for AWS | BellSoft](https://bell-sw.com/pages/downloads/alpaquita-gcp/): Access Alpaquita Linux, the Alpine-based build fine-tuned for Java with glibc and musl support. - [Download JDK 8, 11, 17, 21, 25, 26 | Java Builds for Linux, Windows and macOS](https://bell-sw.com/pages/downloads/): Download Liberica JDK, supported OpenJDK builds. Open source Java 8, 11 and more for Linux, Windows, macOS. - [Download Liberica Native Image Kit, NIK 23 (JDK 17), NIK 23 (JDK 21), NIK 25 (JDK 25), Linux, macOS | BellSoft Java](https://bell-sw.com/pages/downloads/native-image-kit/): Download Liberica Native Image Kit for Linux, Windows, macOS. A tool to boost Java and JVM-based applications. Built from open source GraalVM CE and OpenJDK. ## Blog - [The status of Java on Arm](https://bell-sw.com/java/arm/performance/2019/01/15/the-status-of-java-on-arm/): Today, as Arm processors are primarily viewed as targeting the embedded market, and justifiably so, multiple hardware vendors are using this architecture to build server CPUs and to compete with Intel in the cloud and High Performance Computing (HPC) segment. This broadens the variety of Java applications that run on Arm CPUs and adds to the complexity of the Java Arm port itself, as it must support a segmented variety of CPU vendors and workloads. In this post which follows the article written for the Java Magazine, I explore the status of Java and the Java ecosystem on Arm and its evolution. I also discuss some recent developments in Java Arm port features and performance, emphasizing both the server and IoT/embedded deployments. Why Arm? Leaving aside the embedded and mobile markets, where Arm dominates with 32-bit ARMv5, 6, 7 and 8 ISA, it’s no longer stretching the point to say that Arm provides a viable alternative to the markets that are currently dominated by x86 architecture. Unlike CPU vendors like Intel, which focus on shipping processors and evolve the x86 architecture to do so, Arm is primarily an architecture design company selling architectural and core licenses to its customers, which turn it into actual silicon. This allows a great variety of actual implementations of the same architecture to co-exist and compete in different market segments. It is clearly visible from the recent developments of the Arm architecture itself that the focus has shifted to allow competitive Arm-based server CPU designs. In 2016, Arm finalized a 64-bit and 32-bit capable ARMv8-A ISA, targeting both the embedded and server markets. ARMv8-A architecture, which added support for 64-bit and mandated the presence of NEON SIMD instructions, also introduced optional instructions for AES encryption, SHA-1, SHA-256 and CRC32, which some vendors implement to boost cryptographic and checksum performance. Arm did not stop there. In 2017, Arm extended this architecture with the ARMv8.1-A update, most notably adding new atomic instructions. Later, ARMv8.2-A added half-precision floating-point data processing and dot product SIMD instructions. What’s more important, starting with ARMv8.2-A, optional SVE (Scalable Vector Extension) instructions introduced better support for vectorization compared to the NEON instruction set, making the ARMv8 architecture much better suitable for HPC. Recently, ARMv8.3-A added SIMD complex number support and weaker release consistency instructions. The ARMv8 architecture leaves room for vendor design selection to achieve performance, complexity, and power goals. It adopts a relaxed hardware memory model which is weaker than x86-TSO. Thus, one can observe more out-of-order effects. Compared to ARMv7, there are useful concurrency primitives, including the load-acquire and store-release instructions, as well as weaker barrier instructions. But a cautious programmer who follows Java language memory model will not notice these differences, because the JVM hides it inside the implementation. Several hardware vendors contend with Intel in the server market with their ARMv8-based processor designs, and this should already be taken seriously. Some hardware vendors are new to the Arm server ecosystem, such as Qualcomm with its Centriq 2400 offering, or the relaunched Ampere, which possesses assets from APM. Others, like Cavium, which was recently acquired by Marvell, are already established in the Arm server market and have a track record of delivering ThunderX production systems for several years now. They recently released the second generation ThunderX2 systems. ThunderX systems are readily available in the cloud from cloud providers such as Packet and Amazon. The real competition for ARMv8-based server hardware vendors is not the ARMv8 vendors themselves. It is Intel who they all are trying to compete with. And there is more than just the cloud market for Arm: Cray and HPE are shipping HPC solutions based on ARMv8 architecture, making the HPC future of ARMv8 real. As part of the Vanguard program, Sandia National Labs is deploying its Arm-based supercomputer with a theoretical peak of more than 2.3 petaflops. Aside from the CPU core design, ARMv8-based server vendors invest heavily in parallelism and memory bandwidth, while keeping the power consumption low. For example, Qualcomm Centriq 2400 platform is said to have 48 single-threaded cores and 6 channels of DDR4 memory per SoC. Marvell ThunderX2 has up to 64 four-threaded cores in dual-socket configuration (making the total number of hardware threads 256) and 8 channels of DDR4 memory per socket. In the embedded segment, things are more traditional, and most chip makers license the Cortex-A core from Arm instead of building their own. All major Linux distributions support Arm, including Debian, Oracle, Red Hat, SuSE, Ubuntu. All the tooling at the OS and kernel level is already there, stable and ready for production use. Availability of Java on Arm End users will find a good choice of providers of Java and OpenJDK binaries for Arm. Both the ARMv7 and ARMv8 Java ports are fully functional and the sources are available from OpenJDK under the GPLv2.1 license with the classpath extension, which enabled most Linux distributions to include them in their package repository. Sometimes using OpenJDK binaries provided by the Linux package management systems is for some reason not preferred. For instance, if your favorite Linux distribution does not contain the required packages or you are looking for commercial support, there is an excellent choice of different versions of Java/OpenJDK binaries provided by AdoptOpenJDK, Azul, BellSoft and Oracle. At the time this article was published, Oracle only provided JDK 8 binaries for ARMv8 and ARMv6/7, Azul provided binaries for JDK 8 and 11, while BellSoft offers binaries for JDK 8, 9,10 and 11 which, for the Raspberry Pi, include the OpenJFX and Device IO API modules. Azul, BellSoft and most notably Oracle provide supported binaries that comply with the Java SE specification and verify their binaries with the JCK test suite. Speaking of “your favorite Linux distribution.” My team and I advocate for OpenJDK adoption on Alpine Linux as one of the most cost-effective choices you can make for your project. Error-free work of the musl library in Java apps (without a glibc layer) had been a major BellSoft’s project. The release of JDK 16 introduced JEP 386 that welcomed full-fledged support for Alpine Linux musl in OpenJDK. But what about other common Java SE implementations? In July 2021, when this article was updated, Alpine Linux support in the Java industry could be described with Table 1. Table 1. Availability of Alpine Linux musl Java binaries across OpenJDK distributors. Alpine Linux – Java Liberica JDK Oracle JDK AdoptOpenJDK Red Hat build Corretto Zulu 8 11 16 8 11 16 8 11 16 8 11 16 8 11 16 8 11 16 Alpine Linux/x86 ✓ ✓ ✓ – – – – – ✓ – – – ✓ ✓ ✓ ✓ ✓ ✓ Alpine Linux/AArch64 ✓ ✓ ✓ – – – – – ✓ – – – – – – – – – BellSoft is the only developer that provides support for Alpine Linux + Java on AArch64. These are verified builds, production-ready and available for LTS versions (Java 8 and 11). Features of the Java Arm ports Java and OpenJDK Arm ports are mature for production use. The minimum requirement for Java and OpenJDK implementations is seeking conformance with the Java SE Specification by passing the Java SE Compatibility Test Suite (JCK). Arm and ARMv8 ports have reached that level of compatibility long ago and are first-class citizens among Java-supported platforms, along with x86 and SPARC. While it is very important to ensure compatibility of Java implementations, passing the JCK is not the only requirement for a successful Java port. To meet startup and throughput performance expectations, both ARMv7 and ARMv8 ports have C1 and C2 JIT compilers implemented, thus allowing them to produce optimized code that takes advantage of the underlying architecture specifics. On top of that, the -XX:+TieredCompilation is supported and turned on in the Server VM, which allows for leveraging from fast startup and achieving C2 throughput. A full set of GCs is supported in both ARMv7 and ARMv8 ports: ParallelGC, G1, SerialGC, CMS (Deprecated). For embedded use-cases, the ARMv7 port seen in some bundles also carries a lightweight Minimal VM. For JDK 9 or higher, it allows building Java runtime images with a low static footprint using the Jigsaw feature. For example, running OUTPUT=~/out bin/jlink --module-path jmods --compress=2 --add-modules java.base --output $OUTPUT rm -r $OUTPUT/lib/client $OUTPUT/lib/server echo "-minimal KNOWN" > $OUTPUT/lib/jvm.cfg on BellSoft Arm JDK 10, which provides Minimal VM, produces a java runtime with the java.base module with a static footprint as small as 16 Mb. Surprisingly, java.base (maybe with the addition of several other modules) is sufficient for quite a number of Java applications targeted for constrained IoT gateways. For example, a runtime capable of running Apache Felix or Jetty fits into 32 Mb. Over the years, the ARMv8 port received built-in optimized assembly intrinsics for CPU-intensive operations. At present, only several intrinsics present in the x86 port are absent in the ARMv8 port, and the gap is rapidly closing. All the common features which appear in other ports work on Arm, and several specific to the JVM, such as Docker support, AppCDS v2 were ensured to work on Arm as well. See Table 2 for a detailed comparison of major JVM features across upstream x86, ARMv8 64bit and 32bit Arm ports. Table 2. Comparison of major upstream x86 and Arm JVM port features. x86/64 AARCH64 ARM (32-bit) VMs Client Yes No Yes Server Yes Yes Yes Minimal Yes (32 bit) Yes, since JDK 12 Yes JIT C1 Yes Yes Yes C2 Yes Yes Yes TieredCompilation Yes Yes Yes Graal JIT (Experimental) Yes, since JDK 10 Yes, since JDK 11 No GC SerialGC Yes Yes Yes ParallelGC Yes Yes Yes CMS Yes, Deprecated Yes, Deprecated Yes, Deprecated G1 Yes Yes Yes ZGC Experimental In development No Runtime Container support Yes Yes Yes AppCDS Yes Yes, since JDK 10 Yes, since JDK 10 HugePages Yes Yes Yes Numa Support Yes Yes No Serviceability Java Flight Recorder Yes Yes, since JDK 11 Yes, since JDK 11 Performance of AARCH64 JVM port Hardware, OS and the JVM all contribute to the performance of Java applications and benchmarks. Let's dive into the performance of the ARMv8 port, as the server market is where performance matters most. To make a good comparison, it is important to find x86 and Arm server equivalents. Luckily, the recently released Marvell ThunderX2 ARMv8 CPUs provides a comparable Intel equivalent for each SKU based on SPECint2017 rates. From this table, for studying performance, I selected the ThunderX2 CN9975 and its comparable Intel Xeon Gold 6140 single socket system, both equipped with DDR4-2666 memory and running Ubuntu 16.04. Dual socket systems with these CPUs are also available. It’s interesting to note that ThunderX2 CN9975 CPU has 112 threads (28-core system with 4-way SMP), and the comparable Intel Xeon Gold 6140 has 36 threads (18-core system with Hyper-Threading). To assess the performance of the JVM ARMv8 and x86 ports, I used the SPECjbb2015 1.01 and SPECjvm2008 1.01 benchmarks running the OpenJDK 11 EA build 18. All benchmarks were executed 20 times, and mean values were collected. The SPECjbb2015 benchmark was used to obtain an overall score, while the SPECjvm2008 provided additional insights into the performance of ARMv8 HotSpot JVM port. Since this article is not intended to report the best score obtainable on a specific hardware system, but to study the performance of what a typical end user would see, I intentionally did not fine-tune low level JVM parameters or kernel settings on either system. Check the SPEC scores for the processors as reported by the hardware vendors to compare the highest achievable numbers with JVM options tuning. SPECjbb2015 results The SPECjbb2015 1.01 Composite results (Critical-jOPS and Max-jOPS) are presented in Figure 1. The JVM command line options used for these runs were very common for SPECjbb2015 runs: -Xmx24G -Xms24G -Xmn16G -XX:+AlwaysPreTouch -XX:+UseParallelGC -XX:+UseTransparentHugePages -XX:-UseBiasedLocking for ARMv8 and -Xmx24G -Xms24G -Xmn16G -XX:+AlwaysPreTouch -XX:+UseParallelGC -XX:+UseTransparentHugePages -XX:+UseBiasedLocking for x86. Switching biased locking off for ARMv8 and leaving it on for x86 gave both platforms slightly better results. Figure 1. SPECjbb2015-Composite performance results on single-socket Xeon Gold 6140 and ThunderX2 CN9975 with DDR4-2666 memory running Ubuntu 16.04. Higher is better. As can be seen from the results presented, OpenJDK 11 ARMv8 port running on ThunderX2 outperforms the x86 port on Xeon Gold 6140 by 33% in SPECjbb2015 Max-jOPS score and by 16% in SPECjbb2015 Critical-jOPS score. Long story short, the ThunderX2 system with ARMv8 JVM port is very well suitable for enterprise workloads represented by the SPECjbb2015 benchmark. To assess the per-thread performance, I also limited the number of CPU threads on ThunderX2 to be the same as on Intel Xeon Gold 6140, which only used 32% of its CPU threads. Unsurprisingly, in this case SPECjbb2015 clearly favoured the Xeon Gold, giving it a 30% advantage. SPECjvm2008 results The SPECjvm2008 Base results for individual benchmarks together with the composite Base are presented in Figure 2. Since the SPECjvm2008 “compiler” benchmark has not worked since JDK 8, the composite geomean base score was manually calculated without a “compiler” benchmark result. Figure 2. SPECjvm2008 performance results on single-socket Xeon Gold 6140 and ThunderX2 CN9975 with DDR4-2666 memory running Ubuntu 16.04. Higher is better. As can be seen from the results presented, the OpenJDK 11 ARMv8 port running on ThunderX2 outperforms the x86 port on Xeon Gold 6140 by 28% in SPECjvm2008 benchmark composite Base score. There are two main reasons for the overall better ARMv8 system score. The first is that it has a higher memory bandwidth (8 channels compared to 6 channels on Intel). The second is related to the work done in the ARMv8 Java port that allowed for the full utilization of the CPU potential and extensions. To gain additional insights, let's explore the scores for individual SPECjvm2008 workloads. In eight out of nine SPECjvm2008 benchmarks, the ARMv8 port outperformed x86, and for one the results were the opposite. The Crypto benchmark clearly favors an ARMv8-based system, giving it a 62% advantage, which would not be reachable if the ARMv8 port didn’t fully utilize the AES and SHA extensions available on this chip. The compress benchmark (where ARMv8-based system beats Intel by 12%) uses the CRC32C intrinsic. XML (ARMv8 beats Intel by 29%) and MpegAudio (by 44%) benchmarks use the java.lang.String and java.lang.Arrays intrinsics. Some of these intrinsics were recently improved in JDK 10 and in 11 for ARMv8 by BellSoft together with Cavium/Marvell. It is also important to understand the results for the benchmark where x86 OpenJDK port did better: scimark.small (by 29%). The reason for that is the benchmark code: FFT, LU, SOR and SPARSE scimark subbenchmarks all contain heavy loops and matrix computation code. Over the years a lot of efforts have been made by Intel into loop unrolling and vectorization, which allowed for mapping such code sequences to AVX instructions on x86. This work has not yet been completed for the ARMv8 C2 port, and the absence of a good equivalent to AVX 512-bit is not helping (that gap will be closed when Arm delivers SVE). On top of that, the FFT scimark subbenchmark uses java.lang.Math functions (intrinsified for both x86 and ARMv8, the latter since JDK 11), which use the ARMv8 128-bit NEON SIMD. With scimark.large, this effect is mitigated because scimark.large does computations on a large dataset, which implies memory access, hence giving the ARMv8-based system the possibility to show a wider memory bandwidth. There is definitely some work ahead in order to bring the ARMv8 port scientific workload performance up to par with x86. However, right now it already can be concluded that for regular server-side Java business application workloads (data processing, XML, crypto operations), the OpenJDK 11 ARMv8 port running on ThunderX2 SKUs provides better performance compared to the x86 equivalent. Performance Diagnostics Performance diagnostics tools are essential for understanding the bottlenecks of a Java application being developed or run in production. Regular performance diagnostics through JMX and JVMTI API work on Arm just like they do on x86. For more thorough Java performance analysis, BellSoft also ported the AsyncProfiler and HonestProfiler to ARMv8 and contributed the changes back to the projects. This allowed for enhancing the performance of an application as complex as Hadoop on ARMv8. You can see a subset of the flamegraph from Hadoop Terasort benchmark collected from the Hadoop JVMs using AsyncProfiler on ARMv8 in Figure 3. If the reader is working on a complex Java application and would like to profile the JVM bottlenecks on Arm (or any other architecture), these are the open-source tools I would recommend. Figure 3. Flamegraph built with AsyncProfiler data running Hadoop on ARMv8. JFR, which was open-sourced by Oracle and contributed to OpenJDK 11 was also made available in Arm ports. Figure 4 shows method profiling output from JFR recording on JDK 11 ARMv8. As usual for JFR, the profiling overhead on ARMv8 was low (1-2%) and allowed to obtain detailed profiling information, which is very suitable for production system monitoring. Figure 4. OpenJDK Java Mission Control view of a JFR profile collected from Map Hadoop task on ARMv8. Java Ecosystem on Arm In theory, all software written in Java should just be able to work on Java on Arm. That said, some big projects make specific tweaks that tie them to a specific architecture, such as using natively-built libraries (for example, snappy). The following popular projects, though not claiming official support for ARMv8, were tested to work well on Arm: Hadoop 3.1.0, Tomcat 9.0.8, Spark 2.3.0, Kafka 1.1.0, Cassandra 3.11.2, Lucene 7.3.0, Flink 1.4.2. Future developments A number of companies including Arm, BellSoft, Cavium/Marvell, Linaro, Oracle, Red Hat, and others collaborate in the OpenJDK codebase to ensure the long-term future of upstream Arm ports. That includes gradual improvement in performance and stability, as well as working on a fully-supported Graal VM and Graal as a JIT compiler on ARMv8, ZGC, and such future projects as Valhalla and Panama. Aside from that, work is underway to ensure good use of the planned SVE instructions. Conclusion The upstream ARM 32-bit and ARMv8 Java ports are ready for production use, with all of the relevant features on par with x86. The 32-bit Arm port provides all the necessary functionality for Embedded & IoT deployments, including C1 for fast startup, low dynamic memory footprint and Minimal VM which allows for producing Java Runtime images with a low static footprint (under 16 Mb). It works well on such popular devices as the Raspberry Pi and, after proper device and application specific tuning, it is possible to use the 32-bit Arm port in production under the GPL license. The ARMv8 port which is aimed primarily at the server market, shows better performance results when compared to x86 on equivalent hardware (16% advantage in SPECjbb2015 Critical-jOPS, 33% advantage in SPECjbb2015 Max-jOPS and 28% advantage in SPECjvm2008 base composite compared to x86). As demonstrated by the SPECjvm2008 benchmarks for typical server-side Java business applications which would process and encrypt data and XMLs, the OpenJDK 11 ARMv8 port running on ThunderX2 is faster than the Intel SKU counterpart. The reasons why the SPECjvm2008 scimark.small performance is lower compared to Intel were analysed and should not be too difficult to improve. Overall, the Java software ecosystem is ready for production deployments on Arm. For Embedded & IoT use cases, Arm is already the primary platform of choice, but why would the server market and major cloud providers consider moving to a different architecture if the performance advantage is only tens of percents? The answer to that is price for performance. Considering the performance of the JVM on Arm, and the price of the CPUs, this starts to make sense. And it becomes very easy to try, considering how small the efforts are to take existing Java applications to a new architecture, provided there is a fully functional, performing and stable Java. - [BellSoft at Linaro Connect 19 BKK](https://bell-sw.com/events/2019/02/13/Events-Linaro-Connect-BKK/): Dmitry Chuyko, BKK19-203 JVM on ARM. From sensor to cloud A variety of Java virtual machines on ARM have been around for a long time. Nowadays, OpenJDK makes it easy and secure to receive and process data at all stages. Using modern expressive language Kotlin and Docker containers, we will program a simple, but secure gateway that controls a sensor. We’ll demonstrate further data processing in the cloud as BigData and then visualization for end user using OpenJFX. We will discuss additional features such as deployment and provisioning and also how the new release model of the Java platform is connected to security. Event web site - [JetBrains and BellSoft enter strategic collaboration](https://bell-sw.com/announcements/2019/02/13/JetBrains-BellSoft-Liberica-OpenJDK/): BellSoft will provide security patches and critical updates to JetBrains Runtime in addition to its own Liberica JDK JetBrains Runtime is a runtime environment for running IntelliJ Platform-based products on Windows, macOS, and Linux. JetBrains Runtime is based on the OpenJDK project with some value-add for JetBrains customers. These benefits include Subpixel Anti-Aliasing, enhanced font rendering on Linux, HiDPI support, ligatures, San Francisco font family support on macOS, and so on. BellSoft is one of the Top-5 OpenJDK contributors. They release and support Liberica JDK - an OpenJDK binary distribution verified by TCK for Java SE standard compliance. Liberica JDK 8 and Liberica JDK 11 are long-term supported versions (LTS) which will be receiving all critical patches and security updates at least until 2026. JetBrains collaborates with BellSoft to keep the security and stability level of JetBrains Runtime as high as its done for Liberica JDK. “Working with BellSoft allows us to keep our developers focused on areas, which are critical for our products’ functionality and allow BellSoft engineers to take care of security fixes and critical updates for JetBrains Runtime”,- JetBrains Runtime lead, Konstantin Bulenkov. - [Premium Support is available for Liberica JDK](https://bell-sw.com/announcements/2019/03/26/premium-support-java/): Announcing Premium Support service, BellSoft now offers 24x7 Worldwide Coverage for Liberica JDK Support with 48 hours vulnerability patches SLA. Expanding its Standard Support BellSoft introduces Premium Support service, taking legendary excellence in customer service to new heights. San Jose, The United States of America: BellSoft (USA) has introduced a Premium support service. Since 2017, BellSoft has provided Standard technical support during business hours for Liberica JDK — including free security patches and critical updates. Now, BelLSoft’s new Premium Support addresses the high-end service requirements of Cloud providers, Banks, Stock Trading companies, Telecom carriers, and other organizations that need guaranteed response time and timely issue resolution or evening and weekend service. “We’re responding to customers,” said Aleksei Voitylov, BellSoft CTO. “They asked for service-level agreements with guaranteed response time and quick release for vulnerability and critical patches. Our Premium support service enhances Standard Support with global 24x7 coverage and 48 hours SLA for security updates.” Premium Support offers world-wide, around-the-clock service anytime, day or night. BellSoft support staff, which made the company one of Top-5 OpenJDK contributors are available 24 hours a day, 7 days a week to address major or critical incidents anywhere in the world About BellSoft BellSoft was founded in 2017 by former Oracle Java team. BellSoft participates into Java technology evolution, and the company is among Top-5 most active OpenJDK upstream contributors together with Oracle, Red Hat, SAP, and Google. BellSoft provides and supports Liberica JDK - Java binary distribution based on OpenJDK which can be used as an alternative of Oracle JDK 8, 11 and beyond. Liberica JDK is available for a variety of platforms and targets server, cloud, desktop, and embedded use cases. Liberica JDK guarantees compliance with Java SE standard as verified by TCK test suite. BellSoft provides security updates and critical patches for Liberica JDK in parallel with Oracle Java SE CPU updates. The company offers commercial support for Liberica JDK users and is committed to supporting Liberica JDK 8 and 11 until at least 2026. Besides, BellSoft provides services of complex Open Source project optimization, for example, gcc and LLVM compilers, OpenJDK, MySQL database, Hadoop stack, and others. - [Linux package repositories for Liberica JDK](https://bell-sw.com/announcements/2019/03/28/linux-repositories/): Announcing public availability of repositories for .deb and .rpm-based Linux distributions Keeping JDK secure requires ease of update and tight integration with operating systems default installation techniques. Aside from the already available Docker repositories, we are happy to announce public availability of YUM and APT Linux packages reporitories for all supported versions of Liberica JDK, which includes security patches and critical updates. This allows customers and users to ensure security and integrity of Liberica JDK installations. RPM repositories are known to work with supported versions of Centos, RHEL, Oracle Linux, Fedora and openSUSE. APT repositories are known to work with Ubuntu, Debian and Raspbian. Please refer to installation guide for specific Liberica JDK version for detailed installation instructions on using a Linux repository. APT Repository (.deb-based Linux distributions) Add BellSoft official GPG key and setup the repository sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com --recv-keys 32E9750179FCEA62 echo "deb [arch=amd64] https://apt.bell-sw.com/ stable main" | sudo tee /etc/apt/sources.list.d/bellsoft.list Update repositories and install Liberica JDK packages sudo apt-get update sudo apt-get install bellsoft-java11 YUM Repository (.rpm-based Linux Distributions) Add BellSoft official GPG key and set up the repository gpg --keyserver keys2.kfwebs.net --recv-keys 32e9750179fcea62 gpg --export -a 32e9750179fcea62 | sudo tee /etc/pki/rpm-gpg/RPM-GPG-KEY-bellsoft > /dev/null echo | sudo tee /etc/yum.repos.d/bellsoft.repo > /dev/null << EOF [BellSoft] name=BellSoft Repository baseurl=https://yum.bell-sw.com enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-bellsoft priority=1 EOF Update repositories and install packages sudo yum update sudo yum install bellsoft-java11 Available packages For Liberica JDK 8, the following packages are available: bellsoft-java8 contains the full Liberica JDK, including LibericaFX for platforms which support it. bellsoft-java8-runtime includes Liberica JRE, including LibericaFX for platforms which support it. For Liberica JDK 11 and on, the following packages are available: bellsoft-java11 contains the full Liberica JDK, including LibericaFX and a variety of JVMs for platforms which support it. bellsoft-java11-lite includes Liberica JDK with compressed modules and Server VM, without any extra packages. Supported CPU Architectures Packages for all architectures supported by Liberica JDK are available in the repository. At the time of writing, the list is as follows: Liberica JDK 8: amd64, i386, armhf Liberica JDK 11: amd64, arm64, armhf Liberica JDK 12: amd64, i386, arm64, armhf Switching default version of Liberica JDK Liberica JDK packages, when installed change the default association for related executables. Different major versions of Liberica JDK can be installed in parallel in the same environment. The most major version installed becomes the default one. On .rpm-based Linux operating systems, use update-alternatives command to switch between the default installed versions of runtime. On .rpm-based Linux operating systems, use update-java-alternatives command to switch between the default installed versions of runtime. - [BellSoft at JBCNConf in Barcelona](https://bell-sw.com/events/2019/04/11/Events-JBCNConf/): Dmitry Chuyko, Do not put all eggs in one container Microservice architecture and containerization have become the standards of modern application development. The challenges that developers face today are different from the problems that we used to solve earlier. Creators of the Java runtime respond to this with the appropriate functionality in the JDK. For example, an inexpensive cloud instance can be quite powerful. And it runs a bunch of containers. Then JVMs running in different containers compete for instance resources. Starting from Java 10, virtual machines know how to live in peace, this work has continued in Java 11. On the other hand, you now need to choose which base image to use. This includes the choice of operating system and Java runtime. OS images vary greatly in size and have their own characteristics, which must be taken into account. Java runtime is also for every taste now. And even within the OpenJDK framework, assemblies from different companies are available with different functionality and size. And you can create a custom runtime image. We will consider the practical application of Java 11 functionality in a container environment typical for popular fram. Event web site - [OpenJDK may have a new home soon](https://bell-sw.com/announcements/2019/04/23/OpenJDK-project-git/): The OpenJDK projects available on GitHub For those who are not just looking for Java download, there is interesting news. You may be heard about Project Skara, which intent is moving OpenJDK repository from Mercurial to Git. Using Git will bring an obvious benefit: modern code management infrastructure with pull requests. However, there might be another positive side effect, the OpenJDK community may attract millennials into Java evolution. Last week Robin Westberg announced at skara-dev mailing list that new OpenJDK projects now have read-only repositories on GitHub. Using GitHub simplifies working with repositories for Java developers who want to better understand the runtime internals. It is easier and faster to clone the repositories to build experimental projects. The following read-only repositories are available today: Current/JDK 13: openjdk/jdk JDK 12 Updates: openjdk/jdk12u JDK 11 updates dev: openjdk/jdk11u-dev JDK 11 updates: openjdk/jdk11u Client Libraries: openjdk/client Project Shenandoah: openjdk/shenandoah Project Tsan: openjdk/tsan Project Amber: openjdk/amber Project Valhalla: openjdk/valhalla Project Panama: openjdk/panama Project Loom: openjdk/loom Project Metropolis: openjdk/metropolis Project Portola: openjdk/portola - [Liberica JDK enhances developer experience with package managers](https://bell-sw.com/announcements/2019/05/15/dev-repositories/): SDKMAN! and Homebrew taps now available Developers all over the world like the ability to have multiple JDK versions installed on their development machines. Choosing the JDK version to work with, being it cutting-edge Liberica JDK release or production-ready supported LTS release has to be easy. Aside from the already available Docker, YUM and APT repositories, we are happy to announce public availability of Liberica JDK in SDKMAN! and Homebrew. Liberica JDK packages for SDKMAN! and Homebrew include LibericaFX, which is based on OpenJFX, and are available for the following architectures: SDKMAN!: Windows x86_64, Mac, Linux x86_64, Linux x86 Homebrew: Mac, Linux x86_64 SDKMAN! cheatsheet Install SDKMAN! as instructed here. List the available versions of Liberica JDK: sdk list java Install desired Liberica JDK version: sdk install java 11.0.3-librca Switch between versions of Liberica JDK (assuming you have several installed): sdk use java 12.0.1-librca Set a sepecific version of Liberica JDK as default: sdk default java 8.0.212-librca Check which version of Liberica JDK you have currently selected: sdk current java Homebrew cheatsheet Install Homebrew as instructed here. Add Liberica JDK tap: brew tap bell-sw/liberica Install Liberica JDK. On Mac: brew cask install liberica-jdk11 On Linux: brew install liberica-jdk11 The list of available Liberica JDK versions in Homebrew can be found at bell-sw/homebrew-liberica - [BellSoft at Oracle CodeOne 2019](https://bell-sw.com/events/2019/06/05/Events-CodeOne2019/): Dmitry Chuyko and Alex Belokrylov, Do not put all eggs in one container We will be happy to meet you at Oracle CodeOne conference in San Francisco, September 15 - 19. It is our honor to share the knowledge and experience we obtained in the OpenJDK project. If you want to meet us, please drop an email at info@bell-sw.com or reach me on twitter @gigabel. Microservice architecture and containerization have become the standards of modern application development. The challenges that developers face today are different from the problems that we used to solve earlier. Creators of the Java runtime respond to this with the appropriate functionality in the JDK. For example, an inexpensive cloud instance can be quite powerful. And it runs a bunch of containers. Then JVMs running in different containers compete for instance resources. Starting from Java 10, virtual machines know how to live in peace, this work has continued in Java 11. On the other hand, you now need to choose which base image to use. This includes the choice of operating system and Java runtime. OS images vary greatly in size and have their own characteristics, which must be taken into account. Java runtime is also for every taste now. And even within the OpenJDK framework, assemblies from different companies are available with different functionality and size. And you can create a custom runtime image. We will consider the practical application of Java 11 functionality in a container environment typical for popular frameworks. Event web site - [Liberica JDK 13 EA with OpenJFX 13 on the Raspberry Pi 4](https://bell-sw.com/announcements/2019/09/12/JDK-JavaFX-Video-Preview/): Using Liberica JDK 13 EA to play Media with OpenJFX 13 on the Raspberry Pi 4 Today we published Liberica JDK 13 Early Access builds on all supported platforms. As it currently stands, BellSoft provides the most number of platforms publicly for latest Liberica JDK compared to other OpenJDK distribution vendors. We encourage users to try the preview of all the new features of OpenJDK 13: Dynamic CDS Archives ZGC: Uncommit Unused Memory Reimplement the Legacy Socket API Switch Expressions (Preview) Text Blocks (Preview) With Liberica JDK 13 we will be celebrating the 6th major release of Liberica JDK, so we want to thank our supporters and users with a special gift: Liberica JDK 13 EA now supports Raspberry Pi 4, the most popular single board computer among enthusiasts. This version adds much more processing capabilities compared to Raspberry Pi 3, that’s why we decided to extend the Java FX Modules supported on the Raspberry Pi by adding OpenJFX Media support on top of OpenJFX Graphics and Controls. Liberica JDK on the Raspberry Pi also now supports Raspbian Buster OS release. Since Liberica JDK 13 EA builds for ARM 32 Hard Float now include OpenJFX Media, we encourage users to try it out on the Raspberry Pi 4 using the following steps: Install Raspbian Buster image on the Raspberry Pi 4. Disable “dtoverlay=vc4-fkms-v3d” boot setting in /boot/config.txt by commenting the relevant line and reboot the Pi. Run “sudo apt-get install libavcodec-extra libavformat58 libavcodec58 gstreamer1.0-libav” to install the gstreamer backend and relevant codecs. Install Liberica JDK 13 EA from here. Download an H.264 Video sample from here. Compile the following VideoTest example with javac. Or, better yet, develop your own! Run the VideoTest example, passing the Video sample as command line parameter: java VideoTest simpsons_movie_trailer.mp4 VideoTest.java: import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import java.io.File; import javafx.geometry.Pos; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.media.Media; import javafx.scene.media.MediaPlayer; import javafx.scene.media.MediaView; import javafx.scene.text.Text; public class VideoTest extends Application { static String arg0; public static void main(String[] args) { arg0 = args[0]; Application.launch(args); } @Override public void start(Stage primaryStage) { StackPane root = new StackPane(); MediaPlayer player = new MediaPlayer( new Media(getClass().getResource(arg0).toExternalForm())); MediaView mediaView = new MediaView(player); root.getChildren().add( mediaView); Scene scene = new Scene(root, 1024, 768); primaryStage.setScene(scene); primaryStage.show(); player.play(); } } On the Raspberry Pi 4 we noticed OpenJFX could play 720p and 1080p video with the same quality as using native mplayer/gstreamer. Make sure you use a cooler with your Raspberry Pi 4 - unlike the Raspberry Pi 3, that thing gets hot! In this EA release only the X11 GTK backend and H.264 for video playback are known to work. Adding ES2 and Direct FrameBuffer is in the works, and will be made available as soon as some glitches in the Raspberry Pi 4 video driver are resolved. Video playback takes some time to set up, but after all it feels like a feature you always expected to have. Your feedback is very welcome! - [4 Things you Don’t Want To Do at Code One 2019](https://bell-sw.com/announcements/2019/09/13/CodeOne-2019/): Code One 2019 Next week we’ll be at CodeOne in San Francisco. Hopefully, you will be there too, to learn what’s new and Discover the Latest on Java. To help you on your quest for perfect event experience, we created a shortlist of what you may not want to do at Code One. 1. Don’t miss the Raspberry Pi supercomputer As @chrisbensen was tweeting about Building the Raspberry Pi SuperComputer and blogging about the Very Large Raspberry Pi Cluster, we got more and more excited! Many have asked why would someone ever connect over a 1000 Raspberry Pi’s? Well, it’s the wrong question. Better to ask: “Why wouldn’t you connect them if you can?” :) We are huge fans of Raspberry Pi and recently released the early access of Liberica JDK for Raspberry Pi 4. We want to see the supercomputer and make many selfies with it and trust us, and you don’t want to miss this Code One attraction too! 2. Don’t sleep Sleep is healthy, sure. However, sometimes, there is not enough time for that. The exhibition floor is exciting, with many vendors to speak with and later visit the offsite events. The sessions are beneficial. Moreover, the iconic Golden Gate views are there to take your breath away. So sleep can wait a bit. 3. Don’t be afraid to speak with people Networking is so popular that some people stay away from it. We have a different feeling. At such a huge developer event as Code One, you want to learn more and get to know more people that love the same things as you do. Like, Java. So Code One is just great to be more engaged with the community and get involved. You can volunteer for Code One 4 Kids to help the Next Generation of Developers with their Workshop Projects. Getting involved is a great way to meet people who have similar ideas and similar concerns as you do. Sharing stories with them is one of the most valuable things about Code One. 4. Our last “Don’t” is a bit self-serving Don’t miss the BellSoft Booth 3112 We worked hard to give you Liberica JDK and the Guardians of the Enterprise experience at Code One. Fun superhero tees along with talks with the developers of Liberica JDK and the top-5 contributors to OpenJDK and a super-cool demo with the Nvidia Jetson Nano platform. NVIDIA Jetson Nano is a low-power platform which can be used to create AI systems. For instance Network Video Recorders (NVRs), home robots, and intelligent gateways with full analytics capabilities. The Nvidia Jetson is ready and set at BellSoft boot 3112 for pass-through people traffic count. This installment helps participants of Code One receive verified data about their booth popularity within the conference. Technically we have a Jetson Nano board with 4K camera which captures video and uses DeepStream library for real-time video analytics & people identification. Java application is used to perform data analysis, and the OpenJFX application is visualizing the stats. Come to our Booth 3112 at Code One to learn more about Liberica JDK and the Guardians of the Enterprise, see Nvidia Jetson Nano in use, and make new friends! - [Liberica JDK is generally available on PowerPC](https://bell-sw.com/announcements/2019/09/25/Java-on-PowerPC/): Based on OpenJDK and verified by TCK for Java SE Standard Compliance, Liberica JDK for PowerPC is designed to deliver a more efficient end-to-end developer experience by making the runtime systems more hardware-agnostic. BELLSOFT ANNOUNCES THE GENERAL AVAILABILITY OF LIBERICA JDK ON POWERPC TO INCREASE THE RANGE OF SUPPORTED HARDWARE PLATFORMS San Jose, CA, September 12, 2019 — BellSoft, the world’s top 5 contributor to Java OpenJDK and provider of open-source solutions, announced today the general availability of Liberica JDK for all supported versions of PowerPC. Releasing Liberica JDK for PowerPC makes BellSoft the world’s leader by the number of publicly supported platforms for JDK 11. By announcing the general availability of Liberica JDK for PowerPC, BellSoft increases the range of supported hardware platforms of its OpenJDK binary distribution. Liberica JDK customers are now able to take advantage of one-vendor supported Java runtime across all popular architectures. In addition to the already supported x86, ARM, and SPARC, Liberica JDK runs on PowerPC 8 and 9 servers and embedded solutions powered by PowerPC architecture. Liberica JDK for PowerPC is an underlying runtime responsible for secure and cross-platform application execution. By integrating Liberica JDK 11 natively into the PowerPC Linux platform, developers can improve their application modularity and leverage the new Java language features. Platform operation teams can benefit a TCK-verified JDK 11 runtime with LTS support. Users can benefit from freely available JDK 12 Runtime on PowerPC built on open-source software, which includes all the latest features. As with the features and components of Liberica JDK for PowerPC, Liberica JDK is backed by BellSoft’s support, which makes it a powerful option for production systems in even the most mission-critical roles. Additionally, BellSoft services are available, providing technical expertise and strategic advice to customers of Liberica JDK for PowerPC. Availability Liberica JDK for PowerPC is available for download on the BellSoft website: https://bell-sw.com/pages/downloads/?version=java-11 Learn more with BellSoft at Oracle CodeOne 2019, booth 3112, September 15-19 in San Francisco. Supporting quotes Alex Belokrylov, CEO of BellSoft “Liberica JDK gives the full power of running Java applications across a variety of popular CPU architectures both on-prem and cloud-based. Organizations running a multi-vendor infrastructure will benefit from using publicly available, supported and Java SE Standard Compliant binaries for x86, ARM, SPARC, and PowerPC architectures.” About BellSoft BellSoft releases and supports Liberica JDK, an OpenJDK binary distribution verified by TCK for Java SE Standard Compliance. BellSoft engineers have been contributing to the OpenJDK project since its inception. Being one of the top 5 OpenJDK contributors, BellSoft drives a community-powered approach to deliver reliable and compact containers with Liberica JDK targets microservice solutions on. Using popular as well as in-house performance optimization techniques and tools allows BellSoft to work on performance tuning for industry leaders. - [Oracle Code One 2019: Side Notes](https://bell-sw.com/announcements/2019/09/29/BellSoft-at-CodeOne2019-side-notes/): On September 16-19, the Oracle Code One conference 2019 threw open its doors to Java developers hailing from all over the world. It encompassed all possible subjects a developer might be interested in: from cloud computing, data science, and the professional community to new tools, devices, and technologies. Just like in 2018, the conference was attended by more than 7,000 people, which makes it the world’s most popular event for software developers. For BellSoft, it all started with Geek Bike Ride through San Francisco, a traditional cycling trip for Java enthusiasts which dates back to the time when the conference was called JavaOne. This ride established positive energy continuing throughout each day of the conference. As the OpenJDK project switched to a twice-yearly release calendar, delivering a new version every March and September, one of the most anticipated highlights of the conference was the launch of the new Java 13. Georges Saab, Oracle’s VP, made the announcement on September 17th and presented the new version’s key features: Z Garbage Collector enhancements, application class-data sharing, and previews of switch expressions and text blocks. Of course, this wasn’t a surprise for us, and we released our Liberica JDK 13 in no time at all. Shortly before the conference, we also dropped the news of the general availability of Liberica JDK for PowerPC, which made BellSoft the world’s leader by the number of publicly supported platforms for JDK 11. But really for us, this high profile summit is not just another chance to stay on top of the news about the ever-evolving Java ecosystem, but also a venue to rub elbows with our friends from the Java development team and the hyper-engaged community of Liberica JDK users. Speaking of the latter, so many unique projects are unfolding within the Java ecosystem as a whole, and especially Liberica JDK! For instance, it was nicely encouraging to see the team of Robo4J; they’re developing a framework for robots and IoT devices based on Liberica JDK. If you run your product on Liberica JDK too, please tag us anywhere online by including #LibericaJDK #BellSoft in your post on Twitter. We are eager to learn about more projects using Liberica JDK! We’re already looking to the horizon for the next Code One event, as 2020 only promises to bring a good many terrific new features to the platform. San Francisco, we can’t wait to see you again! - [Meet BellSoft performance engineer at Devoxx Belgium](https://bell-sw.com/events/2019/10/03/Events-Devoxx-Belgium-2019/): Java on ARM. Theory, Applications and Workloads, Dmitry Chuyko, Wednesday from 15:10 to 16:00 in Room 6 There are many great events for developers around the world. However, for the European Java community, there is an outstanding one. It is Devoxx Belgium. If you are there, pay attention to Dmirty Chuyko talk about today and tomorrow of OpenJDK on ARM. We are not talking about Raspberry Pi; Arm is much more than that. Can you imagine a server with hundreds of cores perform on the same level as other CPUs but with lower power consumption? It is a reality which Dmitry will explain in detail. In his talk “Java on ARM. Theory, Applications and Workloads” scheduled on Wednesday from 15:10 to 16:00 in Room 6 https://devoxx.be/talk/?id=13052 he explores the status of Java and the Java ecosystem on ARM, together with the Java ARM port features and performance of specific workloads. We will be happy to meet you at Devoxx! - [Liberica JDK optimized packages introduction](https://bell-sw.com/announcements/2020/01/15/Liberica-JDK-8u242-11.0.6-13.0.2/): Today, with the announcement of the general availability of Liberica JDK 8u242, 11.0.6, 13.0.2, BellSoft introduces several changes that will help Liberica JDK customers have a better experience with the product. Since the first release that happened in 2017, we continue to work on the improvements that make Liberica JDK the most useful and practical OpenJDK distribution in the world. Today, we extend the binary package offering and will be delivering Liberica JDK in three forms: Full, Standard, and Lite. The Full version of Liberica JDK includes all production-ready features of OpenJDK and some add-on packages, for example, OpenJFX, MinimalVM, Device Input-Output API on selected platforms. The Standard version targets most of the use-cases of OpenJDK, including both server and desktop. Lite is the tiniest version of Java SE standard verified OpenJDK binary created to allow high-density deployments in cloud infrastructure. As you may know from our conference talks, we use a hybrid environment to build and test Liberica JDK. And we know from practice that cloud infrastructure is a costly resource that has to be used wisely. Liberica JDK allows you to save your cloud resources by keeping only the required components within the distribution. Now you have even more freedom of choice to decide on Liberica JDK’s form that ideally fits the unique environment you have. - [Meet BellSoft engineers at JFokus in Stockholm](https://bell-sw.com/events/2020/02/02/Events-JFokus-Stockholm/): Check how small can be a Docker container with fully functional JDK at BellSoft Booth. It is the second time BellSoft is going to JFokus to meet a vibrant Sweden Java community. Find us on February 4-5, 2020 in Stockholm at Waterfront Conference Center. At BellSoft booth, you can meet engineers who made the smallest Docker container with Java SE standard verified Liberica JDK, which allows a higher density cloud deployments and saves valuable resources. If you want to know, many useful OpenJDK features for a containerized application join the talk of Dmitry Chuyko on February 4 at 15:35, Room A2, Don’t put all your eggs in one container. See you at JFokus! - [Liberica Mission Control 7.1 released](https://bell-sw.com/announcements/2020/02/04/Java-Mission-Control-7-1/): Liberica Mission Control (LMC) 7.1 is generally available for free download from the BellSoft website. LMC provides developers and production monitoring teams with comprehensive insights into Java runtime and helps identify performance issues on the go with minimal overhead. Liberica Mission Control represents the data collected or recorded live during Java application runtime. It is compatible with Liberica JDK 11+ and other OpenJDK builds, including Oracle Java SE with the Java Flight Recorder (JFR). Liberica Mission Control helps you diagnose how your applications execute in production, so you can observe and troubleshoot Java performance pitfalls that happen under real-world circumstances, for instance, OutOfMemoryError exceptions and lock contention. Even more, it could sometimes highlight lines of code that you were not even aware were adding worthless overhead to your programs. Liberica Mission Control 7.1 brought more than 60 enhancements, performance, and critical updates and is available for Windows, Linux, and macOS. Liberica Mission Control is supported within the Liberica JDK support subscription. - [Liberica JDK 14 is generally available](https://bell-sw.com/announcements/2020/03/18/Liberica-JDK-14/): By changing the cadence of OpenJDK releases two years ago, Oracle accelerated the evolution of Java, which was categorically a positive move for the platform and the ecosystem. Before the change, developers were waiting for the new version with stable features for at least three years. Today, we can admit that the Java run-time development process is iterative. Initially, new capabilities are injected into OpenJDK as preview features; the developer community is testing and giving its feedback to JEP leaders, and after a number of releases, the feature becomes standard. JDK 14 has sixteen new features in six areas: API, Runtime & Performance, Serviceability, Tools, Improved platforms support, and Deprecation. API enhancements JEP-361: Switch Expressions (Standard) JEP-305: Pattern Matching for instanceof (Preview) JEP-359: Records (Preview) JEP-368: Text Blocks (Second Preview) JEP-370: Foreign-Memory Access API (Incubator) Run-time and Performance JEP-358: Helpful NullPointerExceptions JEP-352: Non-Volatile Mapped Byte Buffers JEP-345: NUMA-Aware Memory Allocation for G1 Serviceability JEP-349: JFR Event Streaming Tools JEP-343: Packaging Tool (Incubator) Improved platforms support JEP-364: ZGC on macOS JEP-365: ZGC on Windows Deprecation JEP-362: Deprecate the Solaris and SPARC Ports JEP-366: Deprecate the ParallelScavenge + SerialOld GC Combination JEP-363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector JEP-367: Remove the Pack200 Tools and API Let’s look at some new features in details. API enhancements Switch Expressions (Standard) JEP-361 This feature extended switch statement, which can be used as an expression with the help of arrow (->), and now can yield/return the value. Before JDK 14 switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); } switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } JDK 14 JDK 14 makes the syntax more precise and removes the boilerplate. switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); } Pattern Matching for instanceof (Preview) JEP-305 Before JDK 14 if (o instanceof Foo) { Foo f = () o; System.out.println(f.getName()); } JDK 14 if (o instanceof Foo f) { System.out.println(f.getName()); } This is the first step to other changes. But it’s already possible to combine Switch expressions with instanceof pattern matching and create much cleaner code. Before JDK 14 @Override public void onReceive(Object msg) { if (msg instanceof ClusterMessage) { onMsg((ClusterMessage) msg); } else if (msg instanceof RpcBroadcastMsg) { onMsg((RpcBroadcastMsg) msg); } ....... } JDK 14 @Override public void onReceive(Object msg) { switch (msg) { case ClusterMessage cm -> onMsg(cm); case RpcBroadcastMsg rbm -> onMsg(rbm); ... } } Records (Preview) JEP-359 Before JDK 14, a data class would look like below with getters, setters, equals, hashcode, toString, and so on. To somehow get rid of all that mess, a widely adopted Lombok library was created. Also, you could rely on your IDE. public class Station { private String name; private Coordinates coordinates; public String getName() { return name; } public void setName(String name) { this.name = name; } public Coordinates getCoordinates() { return coordinates; } public void setCoordinates(Coordinates coordinates) { this.coordinates = coordinates; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PlainStation that = (PlainStation) o; return Objects.equals(name, that.name) && Objects.equals(coordinates, that.coordinates); } @Override public int hashCode() { return Objects.hash(name, coordinates); } @Override public String toString() { return "PlainStation{" + "name='" + name + '\'' + ", coordinates=" + coordinates +s '}'; } } With JDK 14, the same data-class can be declared as Record with only one line of code. The class will already have all getters, setters etc. public record RecordStation(String name, List<Coordinates> coordinates) {} Text Blocks (Second Preview) JEP-368 In JDK 13, Text Blocks was introduced with JEP-355. JDK 14 evolves (or, better say, reconsiders) this feature, which is useful for some DSLs. For example, we used it for JOCL (Java binding for OpenCL) private static String vertexShaderSource = """ #version 150 core in vec4 inVertex; in vec3 inColor; uniform mat4 modelviewMatrix; uniform mat4 projectionMatrix; void main(void) { gl_Position = projectionMatrix * modelviewMatrix * inVertex; } """; The new Java language features have well-known analogs in Kotlin. Switch Expressions –> When Expression Records –> Data Classes Text Blocks –> Multiline String Literals These features are already supported since IntelliJ IDEA 2020.1, while JetBrains engineers, together with Oracle and other OpenJDK contributors, worked on feature design and implementation. Foreign-Memory Access API (Incubator) JEP-370 There are some third-party tools like Ignite, mapDB, and memcached to work with off-heap memory. Now Java will have its native API to do the same with new abstractions MemorySegment, MemoryAddress, and MemoryLayout. Deprecation Evolution is not just about adding new capabilities. Evolution is also a removal of rudiments. A new cadence allows quick disposal of outdated features. In JDK 14, the Solaris port will be decommissioned. Solaris 11 will be the end of support in 2035. Users who need to have Java for Solaris should stay on LTS versions: JDK 8 or JDK 11. To download Java SE verified binaries of Liberica JDK 14, go here - [BellSoft and VMware to Work Together on OpenJDK Evolution](https://bell-sw.com/announcements/2020/06/03/BellSoft-and-VMware-to-Work-Together/): BellSoft, a leading OpenJDK contributor, announced work with VMware, a leading innovator in enterprise software. BellSoft will provide their main product, Liberica JDK and full-service support for VMware Tanzu. BellSoft, being a leading OpenJDK contributor, and VMware will join forces and work closely with the OpenJDK community to bring Java runtime to the next level of usability and performance, maintaining its exceptional reliability. Liberica is an OpenJDK binary distribution verified by TCK for Java SE Standard Compliance. “We want to bring industry-first innovations with our partners to provide customers with fast and professional support, high level of trust and flawless security capabilities. We are really happy to become a partner of such a comprehensive ecosystem and we believe these plans have earned us recognition from today’s leading specialist,” says Alex Belokrylov, CEO of Bellsoft. The beneficial partnership Together these two leading companies and their teams will work on improving the Java platform and developing the user community. VMware customers will benefit from using Liberica JDK, supported in a transparent and Open Source way to run, build, and manage their most critical applications with additional assurance in runtime quality and security. Liberica JDK support team will service VMware’s customer requests and resolve any—even the most complicated—Java runtime problems, including unforeseen security and complex performance issues. VMware will receive a reliable partner with whom they can promptly discuss the most profound issues and non-trivial cases. About Bellsoft BellSoft releases and supports Liberica JDK, an OpenJDK binary distribution verified by TCK for Java SE Standard Compliance. BellSoft engineers have been contributing to the OpenJDK project since its inception. Being one of the top-5 OpenJDK contributors, BellSoft drives a community-powered approach to deliver reliable and compact containers with Liberica JDK targets microservice solutions on. Using popular as well as in-house performance optimization techniques and tools allows BellSoft to work on performance tuning for industry leaders. Website: bell-sw.com About VMware VMware software powers the world’s complex digital infrastructure. The company’s cloud, networking and security, and digital workspace offerings provide a dynamic and efficient digital foundation to customers globally, aided by an extensive ecosystem of partners. Headquartered in Palo Alto, California, VMware is committed to being a force for good, from its breakthrough innovations to its global impact. For more information, please visit www.vmware.com/company.html. - [How to Notarize a Mac Application with Liberica JDK using javapackager](https://bell-sw.com/announcements/2020/06/10/How-to-Notarize-a-Mac-Application-with-Liberica-JDK/): One of the main global trends in IT is the safety of users and preventing the spread of malicious programs, but at the same time, it greatly complicates the life of developers. There are different approaches to solving the issue associated with software security. The most common is the use of an antivirus tool by scanning a computer for malware. A complementary approach was taken by Apple, which introduced the Gatekeeper software to force code signing and check downloaded applications before allowing them to run. The trend is moving towards deanonymization, the violation of anonymity when customers of compromised software have their personal data leaked. The need for notarization arises as a counter to this negative trend by giving users the confidence that a software package was created by an identified developer and checked for malware. Regarding security, an example is Windows Defender, which applies a signing code for verifying the compliance of a program to security requirements. Websites use SSL (Security Sockets Layer) certificates for the purpose of ensuring privacy, authentication, and data integrity in online communications. Apple, on the other hand, introduced notarization infrastructure. Regardless of who is notarizing software, the fact remains that it is an essential, albeit burdensome element of software development imposed by Apple as a requirement now. But there are ways to simplify and even ameliorate the process of notarization for applications in Java and JavaFX that are provided by distributors like Liberica JDK, thus making it a routine procedure, rather than an unpleasant allocation of time and resources. The main advantage of resorting to Liberica JDK is ensuring that applications bundled with the runtime as a single package will be able to pass the notarization procedure smoother and with greater ease. What is notarization? Why is it necessary? Notarization gives users more confidence that the Developer ID-signed software being distributed has been checked by Apple for malicious components. Notarization is not an App Review service, as the Apple notary service is an automated system that scans software for malicious content, checks for code-signing issues, and returns the results to the user quickly. If there are no issues, the notary service generates a ticket for the user to staple to their software. The notary service also publishes that ticket online where Gatekeeper can find it. When the user first installs or runs any new software, the presence of a ticket, either online or attached to the executable file, tells Gatekeeper that Apple notarized the software. Gatekeeper then places descriptive information in the initial launch dialog to help the user make an informed choice about whether to launch the app. Now, since the procedure has become mandatory, beginning with macOS Catalina 10.15, it is impossible to launch a non-notarized application. Liberica JDK is a notarized product. What will that give its customers and users? Liberica JDK is an alternative to Java and acts as the basis for application development and launching Java SE applications. As an officially recognized and certified application, Liberica JDK has gone through all the steps of notarization and provides full notarization compliance of its software. Liberica JDK binaries have already been notarized for a year since last summer. OpenJDK notarization is not an obvious process and requires a deep signing of all binaries and modules. And Liberica successfully passed the notarization procedure. In order to create an application and have it notarized, users need to refer to already notarized software. This is why using Liberica JDK is one of the easiest and most straightforward means of developing applications that will be recognized and notarized by the Apple notary service. By referring to the notarized Liberica JDK package as an alternative to Java, application developers will be able to receive a shortcut in having their software verified in the future. It is quite easy with Liberica JDK having a special build that facilitates the delivery of app bundles. The Javapackager element An important element of the application notarization procedure and an integral part of the Liberica JDK package. The Java Packager tool already comes with the JDK and allows packaging Java applications from the command line, thus being an alternative to other third-party tools. It is important to note that the Java Packager does not automatically produce a Jar file. Many other packager formats are available, including native executable ones tailored for various platforms. Liberica JDK’s Javapackager is ultimately applied during the build process, to create applications that would get easily notarized. Alternatively, it may be used as a standalone tool. The javapackager tool gives us several options: Create a macOS application image (for testing purposes) Create a notarized DMG image (suitable for distribution to end users) Create a notarized PKG installer (suitable for distribution to end users) In this article, we will demonstrate how to perform all three operations. But, if you are uncertain or simply do not want to do them on your own, contact our support team. Click the button below, provide us with your details, and you will get advice from expert architects, free of charge. Request Licensing What if you need to notarize your Java or JavaFX application? A guide Notarizing an application with the Liberica JDK package and the Javapackager is a straightforward and easy process. Remember to follow the steps thoroughly and use an unsigned copy of Liberica JDK. The resulting application will be notarized and have the necessary credentials to comply with the requirements set by Apple. To get the aforementioned unsigned copy of Liberica JDK, click the button below, leave your details, and our support team will gladly help you. If you want to make an app using Liberica JDK and Java Packager, you should walk through several stages: create a jar file containing your application, generate a bundle and notarize it. For the notarization to be successful, it is necessary to get a special build of Liberica JDK 8u252 for macOS, which allows creating a signed application that will be notarized. In this example, it is called bellsoft-jdk8u252+9-macos-amd64-full-nosign.zip. We use a special unsigned Liberica JDK 8u252 because the javapackager cannot re-sign already signed JDK files. Extract the unsigned Liberica JDK 8u252 bundle: unzip ./bellsoft-jdk8u252+9-macos-amd64-full-nosign.zip Also, before creating notarized bundles, do not forget to unlock the developer key: security unlock-keychain -p "${YOUR_PASSWORD}" We will next create installer bundles and illustrate this step with the Recaf app. Building an application JAR file Recaf version 2.0.0 will serve as an example since it has a better architecture to work as a standalone macOS application. First, we should build a Recaf jar file, create an application bundle and notarize it. The notary service verifies the app’s content; it goes through a jar file and checks that all native libraries in an executable file are signed. So, our job here is to create a Recaf file, check if it has a native part and sign it. Clone Recaf to your workspace: git clone https://github.com/Col-E/Recaf.git Check out the Recaf 2 branch: git checkout 2.0.0-redesign.10 Compile and package it using the build script or with mvn clean package If your build is successful, the resulting jar file will be placed in the target directory and called recaf-2.0.0-J8-jar-with-dependencies.jar. Java Packager takes the application main class as one of its inputs. If the application’s main class name is unknown, you can easily find it out: Type in the following command to extract the manifest from the jar file: ./jdk8u252.jdk-full-nosign/Contents/Home/bin/jar \ xf ./recaf-2.0.0-J8-jar-with-dependencies.jar \ META-INF/MANIFEST.MF Inspect the extracted MANIFEST.MF to find the main class: grep Main-Class META-INF/MANIFEST.MF The output with the main class name for Recaf is: Main-Class: me.coley.recaf.Recaf Signing native libraries within the application Without signing all native libraries within an application, it will not get notarized. If your app has no native libraries, skip this step. Otherwise, you should expect a notarization error: The Apple notary service will respond with an error message indicating the presence of an unsigned library. Our sample application, Recaf, does contain a native library libjnidispatch.jnilib inside its jar file. This means we need to sign the native library file in Recaf’s jar for further notarization. Follow these steps: Extract the library from the jar file: ./jdk8u252.jdk-full-nosign/Contents/Home/bin/jar \ xf ./recaf-2.0.0-J8-jar-with-dependencies.jar \ com/sun/jna/darwin/libjnidispatch.jnilib Unlock the key chain: security unlock-keychain -p ${YOUR_PASSWORD} Create an entitlements file for signing the library (we will use the same entitlements as is used in Liberica JDK, see below). Sign the library: codesign -o runtime -s "${IDENTITY}" \ --prefix me.coley.recaf. \ --entitlements ./entitlements \ -vvvv ./com/sun/jna/darwin/libjnidispatch.jnilib Add the signed library to Recaf’s jar file: ./jdk8u252.jdk-full-nosign/Contents/Home/bin/jar \ uf recaf-2.0.0-J8-jar-with-dependencies.jar \ com/sun/jna/darwin/libjnidispatch.jnilib Now you can use the Recaf jar to create and notarize your macOS application with bundled Liberica JDK. Creating a Mac application image with bundled Liberica JDK Liberica JDK's javapackager signs the application, adds the necessary entitlements, secures timestamp and the hardened runtime while creating native macOS images. Should you need help on the options, type in the inline help command: ./jdk8u252.jdk-full-nosign/Contents/Home/bin/javapackager \ -help type - mac.app, pkg etc.> \ <-verbose> Here are all the bundler parameters for Mac Application Image (mac.app): name - App Name - String arguments - Command Line Arguments - List mac.bundle-id-signing-prefix - Bundle Signing Prefix - String classpath - Main Jar Classpath - String mac.signing-key-developer-id-app - Apple Developer ID Application Signing Key - String icon.icns - .icns Icon - File jvmOptions - JVM Options - List jvmProperties - JVM System Properties - Map mac.category - Category - String mac.CFBundleIdentifier - CFBundleIdentifier - String mac.CFBundleName - CFBundleName - String mac.CFBundleVersion - CFBundleVersion - String runtime - JRE - RelativeFileSet applicationClass - Main Class - String mainJar - Main Jar - RelativeFileSet preferencesID - Preferences ID - String userJvmOptions - User JVM Options - Map appVersion - Version – String The next step is to create a Mac native application image with the following command: ./jdk8u252.jdk-full-nosign/Contents/Home/bin/javapackager -deploy \ -native image \ -name Recaf \ -outdir res_image \ -srcfiles ./recaf-2.0.0-J8-jar-with-dependencies.jar \ -outfile recaf \ -appclass me.coley.recaf.Recaf \ -Bruntime=./jdk8u252.jdk-full-nosign/Contents/Home/ \ -BappVersion=2.0.0-liber Besides, you may pass your entitlements file using the -Bmac.entitlements=\ option. If the entitlement file is not specified, the following entitlements are used: version="1.0"> com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-executable-page-protection com.apple.security.cs.disable-library-validation com.apple.security.cs.allow-dyld-environment-variables The resulting image will be created in the res_image/bundles/Recaf.app directory. You can ensure that your application is signed and has the necessary properties/options with the command: codesign -dv --entitlements :- ./res_image/bundles/Recaf.app/ It should produce the following output: Executable=***/res_image/bundles/Recaf.app/Contents/MacOS/Recaf Identifier=me.coley.recaf Format=app bundle with Mach-O thin (x86_64) CodeDirectory v=20500 size=378 flags=0x10000(runtime) hashes=3+5 location=embedded Signature size=8998 Timestamp=*********************** Info.plist entries=15 TeamIdentifier=8LBATW8FZA Runtime Version=10.14.0 Sealed Resources version=2 rules=13 files=5 Internal requirements count=1 size=176 version="1.0"> com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-executable-page-protection com.apple.security.cs.disable-library-validation com.apple.security.cs.allow-dyld-environment-variables Given that application images are most suitable for testing and not typically used directly in distribution, we will now focus on creating DMG or PKG bundles tailored for end users. Creating a DMG bundle Choosing between DMGs and PKGs depends on your preference. While these two are perfect for end-user distribution, they possess slightly different qualities. Some developers and vendors ship software in both formats, or go for wrapping a PKG inside a DMG. DMG is effectively a disk image the end user can click on and install an application in the same way as when inserting a CD. Issue the following command to create a DMG image with javapackager: ./jdk8u252.jdk-full-nosign/Contents/Home/bin/javapackager -deploy \ -native dmg \ -name Recaf \ -outdir res_dmg \ -srcfiles ./recaf-2.0.0-J8-jar-with-dependencies.jar \ -outfile recaf \ -appclass me.coley.recaf.Recaf \ -Bruntime=./jdk8u252.jdk-full-nosign/Contents/Home/ \ -BappVersion=2.0.0-liber The ready DMG image will be placed in ./res_dmg/bundles/Recaf-2.0.0-liber.dmg. Then submit the newly made DMG bundle to Apple for a standard procedure and get it notarized. Apple provides a set of tools for notarizing bundles. The two important ones are altool (to interact with the notary service) and stapler (to work with notarization results and staple the bundle afterward). Send the DMG bundle to the Apple notary service by typing in the command: xcrun altool --notarize-app \ -primary-bundle-id com.bell-sw.recaf.0 \ -username ${USERNAME} \ -password '${YOUR_PASSWORD}' \ -file ./res_dmg/bundles/Recaf-2.0.0-liber.dmg You should get the response: 2020-04-27 08:43:58.152 altool[47259:19144203] No errors uploading 'res_dmg/bundles/Recaf-2.0.0-liber.dmg'. RequestUUID = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx At this point, you need to wait for a response from the notary service and check the notarization request status occasionally using the provided request UUID: xcrun altool --notarization-info ${RequestUUID} \ -u ${USERNAME} \ -p '${YOUR_PASSWORD}' Please note that you will have to wait a while for the notarization status. If requested right away, instead of the status, an error will show up saying the request UUID is not found. This is what you will see when the service processes your bundle: *********************** altool[47696:19145453] No errors getting notarization info. RequestUUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Date: *** Status: in progress LogFileURL: (null) Once the bundle is verified, you will get the ‘success’ status, and the package becomes approved: *********************** altool[88539:21766482] No errors getting notarization info. RequestUUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Date: *** Status: success LogFileURL: https://osxapps-ssl.itunes.apple.com/itunes-assets/********* Status Code: 0 Status Message: Package Approved By following the LogFileURL link, you will get a formatted JSON response: { "logFormatVersion": 1, "jobId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "status": "Accepted", "statusSummary": "Ready for distribution", "statusCode": 0, "archiveFilename": "Recaf-2.0.0-liber.dmg", "uploadDate": "********************", "sha256": "3d742fbfbbf6252a6d35eb23114b387934dac1c0be423b357c0b257026cd1fba", "ticketContents": [ { "path": "Recaf-2.0.0-liber.dmg", "digestAlgorithm": "SHA-256", "cdhash": "56e2fa7437a638721fed896a24880607ffca619d" }, { "path": "Recaf-2.0.0-liber.dmg/Recaf.app", "digestAlgorithm": "SHA-256", "cdhash": "8739a51515aded06148e434b735ddf7147d4821a", "arch": "x86_64" }, ... ], "issues": null } Now that the Apple notary service has approved the bundle and generated a special ticket, staple the bundle with the command: xcrun stapler staple ./res_dmg/bundles/Recaf-2.0.0-liber.dmg The output of the ticket stapling command should look like: Processing: *****/res_dmg/bundles/Recaf-2.0.0-liber.dmg Processing: *****/res_dmg/bundles/Recaf-2.0.0-liber.dmg The staple and validate actions worked! Validating the DMG bundle You can also validate your bundle separately with the command: xcrun stapler validate ./res_dmg/bundles/Recaf-2.0.0-liber.dmg This should produce the following output: Processing: *****/res_dmg/bundles/Recaf-2.0.0-liber.dmg The validate action worked! If the validation is successful, you should be able to install the application! Creating a PKG bundle As we said before, it is required to choose either a DMG or PKG file to create at this stage. You may opt for PKG as this installer format provides sophisticated means to build an application installation experience. The instructions for making a notarized PKG file using the javapackager are very similar to those for the DMG one. Type in this command to generate a PKG bundle for Recaf: ./jdk8u252.jdk-full-nosign/Contents/Home/bin/javapackager -deploy \ -native pkg \ -name Recaf \ -outdir res_pkg \ -srcfiles ./recaf-2.0.0-J8-jar-with-dependencies.jar \ -outfile recaf \ -appclass me.coley.recaf.Recaf \ -Bruntime=./jdk8u252.jdk-full-nosign/Contents/Home/ \ -BappVersion=2.0.0-liber Then you should notarize it by sending it to the Apple notary service: xcrun altool --notarize-app \ -primary-bundle-id com.bell-sw.recaf.0 \ -username ${USERNAME} \ -password '${YOUR_PASSWORD}' \ --file ./res_pkg/bundles/Recaf-2.0.0-liber.pkg The output will look like this: *********************** altool[30652:19480464] No errors uploading 'res_pkg/bundles/Recaf-2.0.0-liber.pkg'. RequestUUID = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Wait a little and check the notarization results: xcrun altool --notarization-info ${RequestUUID} \ -u ${USERNAME} \ -p '${YOUR_PASSWORD}' Please note that it may take some time for the Apple notary service to process the request. When the notarization is complete, this command will return: *********************** altool[30708:19481150] No errors getting notarization info. RequestUUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Date: *********************** Status: success LogFileURL: https://osxapps-ssl.itunes.apple.com/***** Status Code: 0 Status Message: Package Approved You may follow the LogFileURL link to see the notarization details for your bundle: { "logFormatVersion": 1, "jobId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "status": "Accepted", "statusSummary": "Ready for distribution", "statusCode": 0, "archiveFilename": "Recaf-2.0.0-liber.pkg", "uploadDate": "***********************", "sha256": "cf6480a8feb54236a9e0a2894a85c130b373a173bc270e661f111e2df4b65e4c", "ticketContents": [ { "path": "Recaf-2.0.0-liber.pkg", "digestAlgorithm": "SHA-1", "cdhash": "22d41fe89a8af1bcd7e594eab94dce942f822874" }, { "path": "Recaf-2.0.0-liber.pkg/Recaf-app.pkg Contents/Payload/Applications/Recaf.app/Contents/MacOS/libpackager.dylib", "digestAlgorithm": "SHA-256", "cdhash": "81ec29cd1ed5c30b1ac0f9aca785e8389d3b0c16", "arch": "x86_64" }, ... ], "issues": null } Having done that, staple the generated ticket to the application: xcrun stapler staple ./res_pkg/bundles/Recaf-2.0.0-liber.pkg This command will produce the following output: Processing: ***/res_pkg/bundles/Recaf-2.0.0-liber.pkg Processing: ***/res_pkg/bundles/Recaf-2.0.0-liber.pkg The staple and validate actions worked! Validating the PKG bundle You can check the signature in the PKG file by clicking the lock icon in the top right corner. The popped-up dialog informs of all its details. Conclusion Notarizing an application with the Liberica JDK package and the Javapackager is a straightforward and easy process. Remember to follow the steps thoroughly and use an unsigned copy of Liberica JDK. The resulting application will be notarized and have the necessary credentials to comply with the requirements set by Apple. To get the aforementioned unsigned copy of Liberica JDK, click the button below, leave your details, and our support team will gladly help you. Request Licensing Useful links javapackager Notarizing macOS Software Before Distribution Resolving Common Notarization Issues Hardened Runtime Recaf - modern bytecode editor Col-E/Recaf: A modern Java bytecode editor - [Liberica JDK 8 with JDK Flight Recorder support preview is available](https://bell-sw.com/announcements/2020/06/15/Liberica-JDK-8-with-JDK-Flight-Recorder/): JDK Flight Recorder (JFR) is a low-overhead data collection framework for troubleshooting Java applications and the HotSpot JVM, which produces event data flow for further analysis with Liberica Mission Control. JFR was contributed to OpenJDK by Oracle to JDK 9 and became a part of JDK 11 afterward. It was being backported to JDK 8 during the last year and became one of the most wanted features for companies that use JDK 8 in production. The backport is based on the JFR 2.0 implementation in JDK 11. New functionality as JFR streaming API available in JDK 14. To use Liberica Mission Control with JFR in JDK 8u262, you need to use the Early Access binaries for LMC 7.1.2 with version detection patch. Liberica JDK 8u262 EA for Linux x64: bellsoft-jdk8u262-ea+6-linux-amd64.deb bellsoft-jdk8u262-ea+6-linux-amd64.rpm bellsoft-jdk8u262-ea+6-linux-amd64.tar.gz bellsoft-jdk8u262-ea+6-src.tar.gz Liberica Mission Control 7.1.2 EA: bellsoft-lmc7.1.2-ea-linux-amd64.tar.gz bellsoft-lmc7.1.2-ea-macos-amd64.zip bellsoft-lmc7.1.2-ea-windows-amd64.zip - [SoftChalk chooses Liberica JDK to create a reliable and secure educational platform based on OpenJDK](https://bell-sw.com/announcements/2020/06/16/SoftChalk-Chooses-Liberica-JDK/): By leveraging Liberica JDK, SoftChalk receives Apple notarization for its Java 8 client and gets macOS release planning back on track. BellSoft, OpenJDK distribution vendor and one of the top-5 OpenJDK contributors, today announced that SoftChalk, provider of content authoring software, has adopted Liberica JDK. With this product, the company will be able to create a fully secure foundation for its educational platform based on OpenJDK and reach macOS Catalina users. SoftChalk is the leading provider of content authoring software for educators in K12 school districts, colleges and universities. For the past 15 years, educators in more than 1500 educational institutions have used SoftChalk to develop interactive and engaging online lessons for their students. The company’s Mac Java client is based on Java 8. Its weaker libraries left them unable to pass Apple notarization and keep releasing their product for the growing portion of customers on macOS Catalina. Therefore, SoftChalk had to defer multiple releases in a row. They needed an OpenJDK distribution that would support Java 8 and hold Apple notarization. Otherwise, they would have to go through a painful update of the whole JDK to a compliant version. The process would further postpone the release schedule and put the business on hold. SoftChalk adopted Liberica JDK distribution because it integrated with their legacy product line seamlessly and included long-term support with timely security fixes. One of the core decisive factors was that Liberica JDK was scanned for malicious components and code-signing issues and got notarized by Apple on time. This way, SoftChalk received a fully compliant Java 8 foundation for their product and were back on track with the release planning even sooner than they expected. “The foundation’s reliability, control of all vulnerable points and a quick resolution of problems by high-level professionals are the only things that allow companies and their customers to be safe. That is why we are happy that such a large platform as SoftChalk is now based on Liberica JDK. Indeed, today, as never before, stability and safety are important to us. We want to be the ones who make the online world more reliable than offline,” explains Alex Belokrylov. “Inability to get our Java 8 client certified by Apple was a show-stopper, as we were unable to deliver our product to the growing portion of users on macOS Catalina. BellSoft’s quick response and ability to receive Apple notarization helped us release much sooner than we thought we could. We were impressed with the support BellSoft offers and we could not find another distribution that would integrate so painlessly with our product line as Liberica JDK,” says Chuck Woods, Manager, Engineering and Operations at SoftChalk. About BellSoft BellSoft releases and supports Liberica JDK, an OpenJDK binary distribution verified by TCK for Java SE Standard Compliance. BellSoft engineers have been contributing to the OpenJDK project since its inception. Being one of the top-5 OpenJDK contributors, BellSoft drives a community-powered approach to deliver reliable and compact containers with Liberica JDK targets microservice solutions on. Using popular as well as in-house performance optimization techniques and tools allows BellSoft to work on performance tuning for industry leaders. Website: https://bell-sw.com About SoftChalk SoftChalk offers content authoring and hosting solutions that are critical to the success of eLearning initiatives, providing educators with easy, fast, affordable ways to create, manage and share rich, interactive content that engages students in the learning process and inspires learning. Lesson content is web-based and can be delivered in almost any learning management system, content management system, on a web-server, on mobile devices or in the cloud — anywhere for just-in-time learning. Website: https://softchalk.com/ - [JDK Flight Recorder – a gem hidden in OpenJDK](https://bell-sw.com/announcements/2020/06/24/Java-Flight-Recorder-a-gem-hidden-in-OpenJDK/): JDK Flight Recorder, also Java Flight Recorder (JFR) is a mighty powerful feature of Hotspot JVM, although it is not as widely known. JFR is not a novel technology. It takes its origin from JRockit JVM; later, it was ported to Hotspot JVM and introduced in Oracle Java 7. At the time of Java 9, JFR was fully open source, and now it is an integral part of OpenJDK. In this article, I would like to highlight key features of Java Flight Recorder and how it can be useful for Java developers. What is a Java Flight Recorder? Java Flight Recorder is a performance/diagnostic tool that could be a huge time saver for Java engineers while troubleshooting applications at runtime. As the name suggests, Java Flight Recorder collects various kinds of events from JVM runtime and records them in the form of binary log files (further referred to as recordings). In OpenJDK 11, JFR is capable of tracing a few hundred types of events creating a comprehensive JVM runtime picture. Recordings produced by JFR are self-contained files that could be further analyzed in Mission Control. What is Mission Control? Mission Control is essentially a graphical front end working with JFR recordings (binary log files) produced by Java Flight Recorder. Mission Control is also open source, and its source code is hosted on GitHub. JFR recordings are logically straightforward, each one a plain collection of events. For them to make sense, these data need to be post-processed. Mission Control offers several built-in reports outlining various aspects of JVM runtime (e.g., code execution, object allocation, garbage collection, and more). Besides general reports, Mission Control has a number of rule-based heuristics for detecting typical problems of Java applications. Finally, you can use a generic event browser and customizable filters to configure reports tailored for your task. Is Java Flight Recorder just another Java profiler? To a certain extent, features of JFR + Mission Control compete with ones of profilers, this is true. Though comparing it to traditional profilers is not very fair. JFR is a tool of its own kind. It is also a feature of JDK, so other Java profilers besides Mission Control could benefit from JFR capabilities too. For certain profiling tasks (e.g., identifying code hot spots or object allocation profiling), JFR + Mission Control is similar to a typical Java profiler (Visual VM, JProfiler, YourKit profiler). These are good open source Java profilers that you can use to avoid paying for commercial tools. Some features essential for profilers are not covered by JFR. Say, if you want to trace SQL statements being sent to the database, JFR is not your tool. You can send application-specific events to JFR. This is a compelling way to collect telemetry peculiar to your application, though it requires upfront work to place event generation in application logic. Finally, some kinds of JVM telemetry are available only via JFR. How can I start using Java Flight Recorder? JFR is an integral part of OpenJDK now. You do not need to download anything besides JDK or configure JVM in any special way to start using it. Open JDK 11 is the earliest LTS (Long Term Support) release with integrated JFR. If you do not already have OpenJDK with JFR support, you can get a version matching your OS and CPU architecture from Liberica JDK. Liberica JDK is a free and open source binary distribution with High-Powered support from BellSoft, a leading OpenJDK contributor. Liberica JDK supports one of the widest range of platforms, which makes it a Unified Java Runtime for cloud, server, and desktop use. JFR workflow is as follows: start flight recording session with a particular configuration, once the session is active for some time, dump recording to a file, stop the session after the dump (this step is optional, you can leave it active if you plan to take more dumps later). There are three ways to start a flight recording session: Using the jcmd CLI tool included in JDK, start/stop/dump JFR sessions. You would need shell access to the server running your Java application to use jcmd. Using Mission Control, you can do the same for both locally running and remote processes (you would need JMX configured to connect to JVM remotely). It is also possible to activate a JFR session on startup via JVM command-line options (this is very useful if you want to profile applications during startup). Starting a flight recording session also requires a configuration (called a profile). The profile defines which kind of events would be collected during the session. It also determines per probe configuration (frequency for method sampling, duration threshold for I/O events, etc.). There are two built-in profiles, “default” and “profiling.” You can also configure and use custom profiles via Mission Control UI. Once you have a JFR recording file, you would need Mission Control to open it. Mission Control is distributed separately from OpenJDK. I would recommend getting Liberica Mission Control at this stage. A brief overview of Mission Control reports Mission Control has tons of features and a fairly steep learning curve. Oracle’s Java 8 had pre-open source Mission Control 5.5 bundled with JDK. Mission Control 6 was bundled with JDK 9, and it was a dramatic change partially related to open-sourcing of the codebase. This same version, Mission Control 6, introduced automatic anomaly detection. Hints produced by built-in heuristics could be very practical for beginners, but sooner or later you would need to dive into other reporting screens. Mission Control 8 is the latest version, it can analyze recordings from JDK 7+. To launch JMC, you need JDK 11+. Here I would like to give a brief description of key reports available in Mission Control. Method profiling JFR can periodically capture stack traces for running threads in JVM. This technique is known as sampling profiling and available in all Java profilers. Statistical analysis of a large corpus of stack traces gives an excellent picture of where the application spends most of the time down to method name or even line number. This report provides useful visualization for this subset of data collected by JFR. This method is typically used in cases where the application is CPU bound. Sampling helps to identify code that is inefficient or could be optimized for better performance gain. Memory This report outlines a memory usage story of your Java application. It combines multiple data points, with the most important of them being object allocation samples. If enabled, JFR records samples of new object allocation events, including class being allocated, size of object, and stack trace to the point of allocation. Statistical analysis of these samples could reveal a lot about your code. “Memory” reports show top classes of objects being allocated. Information on actual allocated size, including stack trace to the point of allocation, is also available. This report is most helpful if you want to reduce garbage collector pressure or optimize memory usage. Memory profiling is extremely valuable when it quickly identifies the “littering” code in your application and provides ideas for optimization. Lock Instances Contentions and deadlocks is another class of Java problems hard to approach without a good tool. JFR collects events of thread blocking on synchronized sections and other concurrency primitives that can be useful during the investigation of Java concurrency problems. “Lock Instances” report would show you aggregated statistics for these events. “Threads” is another report helpful in tracking down concurrency issues. A built-in “Profiling” configuration has a threshold on the duration for such events. Keep in mind that you may need to lower it to get a more detailed picture. The cost of inter-thread contention becomes a harsh reality for high parallel code running on dozens of cores. Java ships with a few concurrency primitives, including lock-free ones. The contention report helps understand the cost of multithreading in your application and possibly highlights bottlenecks where switching to more elaborate locking is most beneficial. File I/O and Socket I/O Blocking file and socket I/O operations are also traced by JFR. These reports show aggregated statistics built from file and socket I/O events, respectively. It is exhilarating to see which files and how long your application has been reading or at which remote endpoints it has exchanged data. There is a little caveat, though. If using JFR for the first time, you are likely to find both of these reports to be empty or almost empty. The reason is the 10 ms threshold used by default for these kinds of events. If you want to see a full I/O picture, set this threshold to 0 when starting a flight recording session. This report is very good at catching “unexpected” I/O events buried deep inside of layers of libraries and frameworks. Threads In this report, you can see various events (contention related, I/O associated, Java thread state changes) put into a unified timeline. Here you can see the interaction of threads in an intuitive visual way. The ability to zoom into the millisecond level of precision is helpful too. Exceptions Every Java application is producing a handful of exceptions at runtime. Some of them end up in log files, but many are suppressed for one reason or another. Silently swallowed exceptions sometimes lead to many hours wasted during the incident investigation afterward. JFR sees all exceptions (whether they were suppressed or not). There are two kinds of events related. JFR records the total count of exceptions produced by runtime. This metric helps to identify exception misuse. JFR could also record every single exception created, including the stack trace, though you would need to tweak the flight recording session’s configuration to get this level of detail. Garbage Collections GC troubleshooting typically involves digging through GC logs. And of course, you have your GC logging configured properly in your production, don’t you? JFR collects detailed information concerning GC events, and this report visualizes this data. Almost any metric of GC present in JVM is available here. If you need to tune GC or troubleshoot abnormal GC pauses, this is a report to start with. VM Operations HotSpot has a concept of VM operations ranging in types. But the critical point is that all of them require the infamous Stop-the-World pause. With some JVM options, you can enable logging of VM operation or use JFR and this handy report. VM operations are often mistaken for GC pauses (which is just one type of VM operations). People tend to blame Stop-the-World on garbage collectors. This report is your first stop if the application is experiencing Stop-the-World pauses. Configuration and environment A neat thing about JFR files is that you can send them for analysis to an expert in the JVM domain. Files include JVM configuration, hardware configuration, and even a list of other processes running on a server and competing for resources without application. As a JVM guru, I can get almost any context information I need from the recording file itself. Continuous flight recording JFR + Mission Control could be used as a free and capable Java profiler, no doubt. Though using it in continuous mode could bring even more value. To get the full benefit from continuous flight recording, you would need to configure your JVM to be accessible via JMX add JVM command-line arguments to activate flight recorder on startup A minimal command to start flight recording session on startup is below -XX:StartFlightRecording=name=background,maxsize=100m An important option is “maxsize,” which limits how many events would be retained in memory. Now you have JFR silently collecting events in the background. As long as the application runs fine, you can forget about JFR being active. The default JFR profile adds very little overhead. Still, in an unfortunate turn of events, you can quickly connect to JVM via Mission Control and dump accumulated events from a problematic server, which is especially useful for tracking down problems reproducing only on live production. One can argue that instead of pulling data dumps from individual VMs, it is better to have a centralized monitoring system where you can access diagnostics from any server. In theory, that would be incredible, but actually arranging quality centralized monitoring is extremely difficult. Central monitoring solutions have to compromise on the level of details. Continuous flight recording and the ability to pull event dumps on demand are complementary to central logging and general monitoring solutions, as you can get very detailed telemetry from JVM. Plus, it is already built into your OpenJDK, so why not start using it? Java 8 roadblock While Java 14 is already out, many systems are stuck with Java 8, whatever the reason. Unfortunately, Java Flight Recorder is not included in OpenJDK 8 (it is available in Oracle Java but not for free). Java version dependency stands behind the low Java Flight Recorder adoption among developers. You need to move to Java 11 to be able to use JFR in OpenJDK. The good news is that Java Flight Recorder has been backported to OpenJDK 8 codebase and will soon be ready to use. Conclusion Java Flight Recorder is a mature technology integrated with modern OpenJDK. JFR + Mission Control offers a reliable alternative to commercial Java profilers for most common profiling tasks. But remember that you can also use open source Java profilers. JFR goes beyond simple profiler workflow with its ability to continuously collect data and access them remotely. As part of the JVM binary, Java Flight Recorder could be easily enabled and does not require any upfront configuration, making it a powerful tool in the Java engineer’s toolbox. So, if you have problems with your Java performance or stability, Java Flight Recorder and Mission Control is a combo you definitely need to give a try. Related Posts Hunting down code hotspots with JDK Flight Recorder Hunting down memory issues with JDK Flight Recorder JVM in Linux containers, surviving the isolation JDK Flight Recorder, The Programmatic Way - [Liberica JDK 8u262, 11.0.8, 14.0.2 builds are released](https://bell-sw.com/announcements/2020/07/14/The-Liberica-8u262-11_0_8-14_0_2-are-generally-available/): Today we are announcing the general availability of Liberica JDK’s three brand-new versions: 8u262, 11.0.8, and 14.0.2. These three releases contain, in total, 765 backports of fixes that address known functional issues (six of them are the backports of BellSoft commits) and incorporate 11 common security vulnerabilities and exposures. In addition to these enhancements, the releases are packed with the following exciting new features from the BellSoft team: 1. Alpine Linux musl support added for Liberica JDK 8. Previously, support for musl-based Alpine Linux was only available on Liberica JDK 11 and 14. We added a backport for JDK 8 with this release, as there are so many companies and developers using JDK 8 in production and eager to leverage Alpine Linux for its resource efficiency and simplicity. This feature will enable users to trim down Docker containers’ size and keep their environment as small and efficient as possible. 2. Javapackager added for Liberica JDK 8 and 11. A feature in OpenJFX, which is now being re-engineered by the OpenJDK community as the jdk.incubator.jpackage module, was made available for Liberica JDK 8 and 11. It allows installing and uninstalling Java applications in a manner that is suitable for a specific platform (whether they are msi and exe formats on Windows, deb and rpm on Linux, or pkg and dmg on macOS), and distributing them in a user-friendly way. With Javapackager being added, our users can now build application images, bundle them with the runtime as a single package, and reduce the installation requirements. 3. Liberica JDK 8 with JDK Flight Recorder is available. JDK Flight Recorder (JFR) is a low-overhead JVM metrics collection framework for troubleshooting Java applications and the HotSpot JVM. This performance/diagnostic tool can be a huge time saver for Java engineers who need insights into how their applications perform while troubleshooting them at runtime. Being backported to OpenJDK 8 during the last year, it surely became one of the most wanted features. The backport will become generally available in autumn, but we decided to create a preview of this release in July for the early birds! To use Liberica Mission Control with JFR in JDK 8u262, you need to use the Early Access binaries for LMC 7.1.2 with version detection patch JMC-6554 available here: bellsoft-lmc7.1.2-ea-linux-amd64.tar.gz bellsoft-lmc7.1.2-ea-macos-amd64.zip bellsoft-lmc7.1.2-ea-windows-amd64.zip and the Liberica JDK binaries from here: bellsoft-jdk8u262+10-windows-amd64-jfr.msi bellsoft-jdk8u262+10-windows-amd64-jfr.zip bellsoft-jdk8u262+10-windows-amd64-full-jfr.msi bellsoft-jdk8u262+10-windows-amd64-full-jfr.zip bellsoft-jdk8u262+10-linux-amd64-jfr.deb bellsoft-jdk8u262+10-linux-amd64-jfr.rpm bellsoft-jdk8u262+10-linux-amd64-jfr.tar.gz bellsoft-jdk8u262+10-linux-amd64-full-jfr.deb bellsoft-jdk8u262+10-linux-amd64-full-jfr.rpm bellsoft-jdk8u262+10-linux-amd64-full-jfr.tar.gz 4. Liberica 11 for AArch64 now contains the Full bundle that includes Liberica FX. In the previous versions, AArch64 support for LibericaFX was limited to Liberica 14. Starting from the 11.0.8 release, Liberica 11 for AArch64 now contains the Full bundle, including Liberica FX. This way, users that are programming on JDK 11 can now launch JavaFX applications on the AArch64-based OSes on SBCs like Raspberry Pi and stay on the LTS version of Liberica. With these new releases, we deliver upon our commitment to enabling more performance with less hassle and administration. Hope you will enjoy them! All builds are ready for download here. - [Optimizing Java Microservices with the smallest container on the market](https://bell-sw.com/announcements/2020/07/14/Optimizing-Java-Microservices-with-the-smallest-container-on-the-market/): BellSoft prepares the final changes to upstream the Alpine Linux port to OpenJDK We have some exciting news! BellSoft’s new JEP 386 is designed to integrate the Portola Project into upstream OpenJDK. This port will allow building a variant of the JDK on Alpine Linux/x64 and other Linux distributions that use musl as their primary C library, so there is no need for the glibc portability layer. The company’s engineering team created a merge from the Portola repository to the JDK mainline branch, fixed several solutions proposed by previous contributors, and thoroughly tested the resulting patch. Alpine is a Linux distribution based on musl as libc and Busybox as a toolset. The greatest advantage of this system is its lightweight nature. Alpine is perfect for container deployments Various sources glorified Alpine Linux as “the new default OS” and “distribution of choice” when it replaced Ubuntu as the base image for Docker in 20161. Thanks to its small image size, this distribution is widely adopted in cloud deployments, microservices, and container environments. For instance, Liberica JDK Alpine musl image is at least 200 MB less than the others available (43.5 MB vs. 228 MB for Debian or even 319 MB for CentOS). With a truly minuscule Docker base image of no more than 6 MB and an embedded Docker build process, it is perfect for minimizing valuable cloud resources. Another positive feature is that the service management subsystem is not even included by default in basic Alpine Linux containers. As a result, it frees up RAM, simplifies diagnostics and system configuration and boosts launch time. At a certain angle, having less contents in a distribution reduces a possible attack surface. Maximizing business value with containers: less is more But what lies behind the need for small images in development? A simple answer is “cost efficiency”. A longer one starts with an understanding that containers are gaining traction each year. Users want to get more value while spending less, that is the gospel truth. And here we can see the absolute superiority of containerization over virtualization. Docker containers are great for streamlining development with the microservice architecture. If you plan to move from Java monolith or create a new solution based on Java microservices, count on our engineers to pave the most efficient migration path. We are offering an entirely free consultation with a BellSoft expert. Fill out the simple form below and schedule a meeting now! Request Licensing First, bug fixes and changes are delivered to the whole application package continuously and fast. Deployment may include many new instances, for example, when the application is dynamically scaled in some region. A major problem that usually arises during this process is high traffic expenditures. Also, the less the image you have to upload, the less time you waste before the application begins to perform business tasks. Second is the age-old problem of storage space. In the case of containers, an isolated environment requires tens of megabytes. In contrast, a virtual volume with full OS will be sized in gigabytes. Just imagine what your company can do with the saved space and money not spent on extra cloud storage, let alone boosted overall performance. Third, waiting for deployment rollout to finish typically takes up huge chunks of programmer’s work. It is easy to calculate profits if this time is reduced to a minimum with containers deployed almost in an instant. Let’s turn to a hypothetical enterprise that transfers to containerization. Reducing overhead charges, no longer having to set up a hypervisor and run a virtual machine… All good. New shiny containers deploy on a clean target device. Why wouldn’t the company want to save even more with smaller images? Which brings it to an idea of JDK containers on an intrinsically lightweight Alpine Linux. Before unraveling the history behind the JDK port to Alpine, we want to say that BellSoft’s binaries (the smallest in the industry!) are supported not only on Linux but on all known platforms. JEP’s background, demand and idea Mikael Vidstedt, now Director of JVM Software Engineering at Oracle, was the first to consider bringing support for alternative implementations of standard C libraries to OpenJDK. The problem was self-evident: say, a person trying to set up a Java VM on an OpenWrt router would soon find it impossible. Mr. Vidstedt decided to change the status quo by starting a venture dubbed the Portola Project in February 2017. He could easily be the one to propose the port to the master branch. But his team had neither sufficiently analyzed the changes (to define their purpose and check other possible solutions) nor done enough testing on Alpine Linux and then all available platforms: Linux, macOS X, Windows, arm, and aarch64, as the patch would affect shared code in JDK. The work on this JEP started a while back when BellSoft understood that containerized JDK 9 is quite favored among users. We supported our Liberica distribution on various platforms and made delivery convenient with a choice of channels: you could download a binary build from the website, install a repository for Linux, download an installer, find publicly available builds on package managers (like SDKMAN), or pick a container image. BellSoft saw the big future in supporting such builds in commercial terms and first created containers for Alpine Linux with glibc compatibility layer. The earliest Liberica version containing Alpine Docker images was 9.0.1 on armhf (for Raspberry Pi). The Portola Project started at times of JDK 10. Portola is about building OpenJDK linked directly to musl. In a musl-based system without the compatibility layer, programs linked to glibc (including regular JDK builds) won’t start. Such builds with a different standard library become some sort of a port. We also should take into account all other peculiarities of target systems, some covered by the existing tests and some not. New containers with OpenJDK took off fast. Liberica OpenJDK statistics on Docker Hub really shows how popular Alpine images are: 100K+ downloads and 10-star rating. The two reasons are that regular JVM images do not work on Linux and that Alpine is inherently lightweight. Given the success of Liberica containers with glibc, we, engineers at BellSoft, decided to take the “intermediary” out of the equation and enter the Portola Project to help produce a rounded and single-purpose JDK for Alpine. In the team’s view, the procedures inside the kit should address the standard library without emulating glibc behavior. We have been shipping Liberica-openjdk-alpine-musl container images since JDK 11. By getting rid of this layer, a new container was expected to save approximately 24 MB—an obvious driver for a company that strives to minimize costs for clients. BellSoft’s contribution to the Portola port Reasonable questions here would be: Why did the JDK not work with musl libc? What changes has BellSoft had to make to its Alpine Linux containers? First, the team prepared a merge from the Portola repository to the JDK master branch, which due to the branch’s ever evolving nature needed to be synchronized non-stop before it was approved and pushed. The patch was split into categories, each one examined and code-named by changes it provides. Some are minor improvements, like defining libc in config: Before: ldd_version=`ldd --version 2>&1 | head -1 | cut -f1 -d' '` After: libc_vendor=`ldd --version 2>&1 | sed -n '1s/.*\(musl\).*/\1/p’` Note: with this improvement, it is also possible to cross-compile the Linux-musl OpenJDK images on traditional Linux x64 based systems using GNU/Linux distributions. Portola cross-compilation: Clone hg.openjdk.java.net/jdk/jdk repository hg clone https://hg.openjdk.java.net/jdk/jdk Apply Portola patch Build jdk for the current platform which will be used as boot and build jdk for cross-compilation mkdir -p build/linux-x86_64-boot-release cd build/linux-x86_64-boot-release sh ../../configure --with-boot-jdk=/jdk-14.0.1 make jdk-image Download cross musl-based toolchain from https://musl.cc Copy sysroot from target system (for example from Alpine Lunux docker image) Configure musl build with devkit and sysroot export DEVKIT=/x86_64-linux-musl-cross export SYSROOT=/alpine3.8-sysroot export TARGET=x86_64-linux-musl export BOOT_JDK=build/linux-x86_64-boot-release/images/jdk sh ./configure --with-jvm-variants=server \ --with-boot-jdk=$BOOT_JDK \ --with-build-jdk=$BOOT_JDK \ --openjdk-target=x86_64-unknown-linux-musl \ --with-devkit=${DEVKIT} \ --with-sysroot=${SYSROOT} Build jdk image cd build/linux-x86_64-server-release make jdk-image Or, instead of static int sigWakeup = (__SIGRTMAX - 2), using #define INTERRUPT_SIGNAL (SIGRTMAX - 2) where SIGRTMAX is a function in musl libc. Others encouraged BellSoft to search for different solutions. One example here is that the musl library does not implement dlvsym since it is not a part of POSIX. The initial Portola Project tries to load dlvsym dynamically and provides fall back in case it is absent. The engineers decided to replace it with dlvsym stub and rewrite the code to: #ifdef MUSL_LIBC static void *dlvsym(void *handle, const char *symbol, const char *version) { return NULL; } #endif Certain solutions did not make the final cut just yet. Such as execvp in musl libc that improperly executes shell files without a valid shebang line—a known issue, actually2. Finally, the team singled out parts of code to submit them for a standalone review and fix independently of the Portola patch. A major and the most labor-intensive part was jpackage, which could even be considered a subproject of sorts. Under particular circumstances, java launcher executor re-executed the application. This is a corner case for musl libc which, when loading shared libraries, does not check them by short name. It was found that the issue with jpackage application re-execution is not specific to Alpine Linux and can be reproduced on other Linux systems that change LD_LIBRARY_PATH3. The other huge chunk of work was testing. BellSoft’s first step was to run JCK, jtreg, vmTestbase and jvmTestbase. The team got cornered a little: we tested the master repository constantly under heavy development. Fails occurred even without the Portola patch. The decision was to run all tests with JDK master branch and use the passed ones as a reference to the Portola build configured for fastdebug. As a result, some fixes were made to the patch before the merge, some already in the merge, and some were reported as musl libc bugs (like the execvp issue discussed above). To recap, by porting the JDK to musl-based Linux distributions, developers will find updating base layers and storing them in a private repository much simpler. Among other benefits are reduced time spent on deployment to a new host, shortened redeploy in general, and saved storage. All these are critical in facilitating business processes and cutting down costs, especially when a business pays for CPU consumed in the cloud. Applying jlink to reduce the size of the Java runtime, a user will be able to create an even smaller image targeted to run a specific application. But sometimes it looks too complex because of an extra step in the deployment pipeline or because of dynamic dependencies. So our alpine-musl docker images can be used in the ‘docker build’ process with extra options. For instance, a user can easily get a runtime that contains either only java.base (to run simplest apps), or some typical modules for a web application, or all the modules. The preparation process is much simpler than the full-fledged jlink tool. A user has no need to recalculate actual dependencies and just chooses between the three lightweight JDK 14.0.1 presets: Base (43.5 MB) / Light (111 MB) / Full (192 MB). The JEP will further support Java runtime not only on Alpine but on all Linux systems with musl. We claim (although some additional testing is still to come) that the JDK port runs on OpenWrt, a popular Linux distribution also based on musl. It powers many small smart devices such as routers. BellSoft guarantees the quality of its Alpine images. The company assures every container image is TCK verified. It is sometimes not so simple to combine things. For example, in some older versions of Alpine, JVM simply shut down on launch because of experimental security patches included. Containers with Liberica JDK, properly tested and verified, show none of these problems. The team is focusing major engineering efforts on this patch, supports the builds, resolves all problems in the timeliest manner, and plans to further follow musl-related part of OpenJDK. Expectations and prospects Overall, the Portola Project is moving at its designated pace towards finalization. However, to become part of the upcoming JDK 16 next year, it still should be refactored and polished. Late June saw BellSoft taking a refreshed JEP implementation for review to fix certain non-musl-related issues. There were several new patches (that are concerned with running jpackage; on AIX they are checked by SAP) integrated into the main branch. After jpackage change was tested by SAP, now, at the time of writing, it is waiting for a public review on core-libs-dev3. Portola polishing changes passed from internal review to public discussion on portola-dev4, where developers already responded to some concerns. Next is to introduce the first change into the mainline JDK, then integrate polishing changes into the Portola repository. The final touch is a public review of all Portola changes going into the JDK mainline branch. BellSoft expects the whole JEP to get processed by mid-August. We see exciting benefits and stability in Portola-powered builds. At the same time, there is a strong demand, as always, for JDK 8. One path for improvement is to build JDK 8 for Linux-musl. The proposed changes apply to the JDK most beloved by the developers (aka “the best release”) since JDK 8u had lots of containerization-related features backported5. It would still be labor- and time-intensive as the current Portola status is downstream. After the JEP is fully integrated, we might find resources to provide this option for the latest updates of version 8. What is next for BellSoft in this area? Our team wants to tackle musl libc for GraalVM within the Graal Project. This library and issues it raises for Java runtime developers are a nice spot for the long-term. Whereas previously the company poured resources into maintaining correctness in new versions of their product, now it is ready to produce novel technical features and support containers. Virtual machines are no longer the talk of the JDK community. Working with containers is seen as mainstream and the primary way to use JavaTM in production. This year and beyond, we promise to dive deeper into the topic and make the developers’ job easier and more enjoyable than it has ever been. And BellSoft likes to keep promises: We are proud to announce that the latest Liberica JDK’s versions, 8u262, 11.0.8 and 14.0.2, include the long-awaited Alpine Linux musl libc support and lots of other exciting features. Download Liberica JDK Thanks to the profound knowledge of this OS, we have created the smallest known containers among OpenJDK contributors. Our customers have used Liberica JDK containers with Alpine musl in production environment for years and admitted savings on cloud traffic, storage and even engineering time. If you want to start working in the cloud and be most efficient, if you are ready to save precious time and money, contact BellSoft right away! Request Licensing Links: Review: Alpine Linux is made for Docker https://www.openwall.com/lists/musl/2020/02/12/9 https://mail.openjdk.java.net/pipermail/core-libs-dev/2020-June/067456.html https://mail.openjdk.java.net/pipermail/portola-dev/2020-June/000441.html GeeCON 2019: Dmitry Chuyko - Do not put all eggs in one container - [Hunting down code hotspots with JDK Flight Recorder](https://bell-sw.com/announcements/2020/07/22/Hunting-down-code-hotspots-with-JDK-Flight-Recorder/): Hunting down code hotspots is probably the most common task for Java profilers. JDK Flight Recorder (JFR) and Mission Control (MC) are free and open source performance/profiling products available with OpenJDK 11. They have a few powerful tools for code execution profiling. In this article, I would like to walk you through the code profiling features they offer. Table of Contents What is a code hotspot? Starting profiling with Mission Control Working with Local JVM Working with remote JVM Before starting the profiler Mission Control JMX Console and Thread monitor What if JMX is not an option? Starting JDK Flight Recorder session Making JFR recording without Mission Control Method profiling report Power of “Stack Trace” view Why CPU usage is important Retrospective thread CPU usage in Mission Control Limit scope to a subset of threads What do I see in the “Stack Trace” view of the “Threads” report? Caveats of JFR method profiling “Cold” code hotspots File I/O report Socket I/O report Lock instances report Threads report Conclusions Related Posts What is a code hotspot? Usually, you are starting a profiler when there is a performance problem, or you want to optimize code to meet specific performance goals. “Performance” is most commonly expressed as execution time (time needed to execute operation) or throughput (number of operations executed per time unit). If you want to reduce the time spent on a request, it is obvious to focus on code that takes the longest to run. This is what we call “hot code” or “code hotspots.” Profilers are great tools for identifying “code hotspots” and JFR + MC is an excellent profiler combo. Let’s launch the application and start profiling, right? Starting profiling with Mission Control We will work with OpenJDK 11 and Mission Control 7.1, JDK Flight Recorder is integrated into OpenJDK 11. You can get the distribution of OpenJDK and Mission Control for your OS from Liberica if you don’t have them already. If you stick with Java 8, Flight Recorder support is available with OpenJDK 8u262. You can find more details in this post by BellSoft. You can use mission control with either a locally running JVM or a remote one. In the latter case, you need JMX port configured on a remote JVM. Of course, you would need an application you will profile and some load to keep it busy. Working with Local JVM Locally running JVM processes are listed in the “JVM Browser” view. You need to identify the JVM you want to profile. Besides, you can either open the JMX console for that JVM or control flight recorder. Working with remote JVM With OpenJDK, Flight Recorder is available remotely via JMX. As might be expected, you would need a JMX socket configured on JVM that you would like to profile. There are two ways to make the JMX socket available: JMX could be enabled via JVM command-line arguments. You can find the required configuration options in the official documentation. Using the jcmd command, you can start the JMX socket on JVM with no up-front configuration and no restart necessary. Below is an example of a command to start JMX with minimal configuration, you would need to know the PID of a process and be logged under the same user as the target JVM. jcmd PID ManagementAgent.start \ jmxremote.authenticate=false \ jmxremote.ssl=false \ jmxremote.port=5555 Once the command is executed, you can connect to your JVM via port 5555 using the instruction in the following pages. If the JVM you want to profile is behind NAT/firewall (e.g., it is running within Kubernetes), you may need to use port forwarding and additional configuration tweaks to make JMX work. Having configured the JMX socket, you need to add remote JVM to it in the “JVM Browser” view. Click the button at the toolbar of that view. You will be prompted to enter JMX connection details. Once you finish JMX configuration, nodes for your remote JVM would appear in the view. Now you can see either a JMX Console or control flight recorder. Before starting the profiler Before starting profiling and focusing on code, it would be nice to get an overall CPU usage picture on the host where your JVM is running. Is the CPU overutilized? Do other processes compete for CPU resources? How much CPU is the JVM process consuming? Which Java threads account for the highest CPU usage? Answers to these questions will help you to choose the right reports in Mission Control later. These questions could be resolved with standard system tools such as top and pidstats, but it is also possible to use JMX console in Mission Control. Mission Control JMX Console and Thread monitor Before starting Flight Recorder, I always recommend looking at the JMX console built into Mission Control. With JMX console, you can both monitor the system and process CPU usage. These metrics are available at the overview tab. You can observe CPU usage per thread as well, which is quite beneficial information for code execution profiling. You need to go to the “Threads” tab and check the “CPU Profiling” box above the table of threads. After this, you could see values in the “Total CPU Usage” column. Pay attention, though, that Mission Control shows the percent of CPU usage from all cores available on the host. As a single thread cannot consume more than a single core, in that table, 100% / N (where N is the number of cores) is the max value you can see. What if JMX is not an option? What if you have console access to JVM, but no way to connect to it via JMX? You can still record JFR files using jcmd, then copy them to your machine and open in Mission Control. I will elaborate on this scenario later in this article. Obviously, the JMX Console provided by Mission Control would not be useful in this case, but you can use console tools such as pidstat or sjk to monitor per-thread CPU usage. Starting JDK Flight Recorder session Let’s start the Flight Recorder now. Right click on the “Flight Recorder” node in the tree on the “JVM Browser” view and select the “Start Flight Recording …” option. You will see a dialog with options to start the Flight Recorder session. This screenshot reflects starting Flight Recorder for 2 minutes, with “Profiling - on server” default settings. Now you can hit the “Next” button and get another dialog allowing you to tweak the most common Flight Recorder options for the next recording session. Some of these options would affect the reports explained in this article: “Method Sampling” — this option controls the frequency of stack trace sampling. “Maximum” means 100 samples per second, which is a good trade-off between performance and data quality. “Exceptions” — you can choose whenever to record all Throwables or just Errors. Recording all Throwables may be expensive, but may be useful to pinpoint excessive exception usage in application. Thresholds “Synchronization”, “File I/O”, “Socket I/O” — for detailed analysis of “cold” hotspots, I would recommend you set these thresholds to zero. Though it may increase overhead significantly, so do it only if you really need this information. Now let’s hit “Finish” to start the Flight Recording session. You can click “Next” again, although, in that case, you would have a chance to view/edit Flight Recorder configuration at a lower level. After starting the Flight Recorder session, Mission Control would wait for the specified duration, then stop Flight Recorder, dump the data file, open it and present several reports. Making JFR recording without Mission Control JFR recording could be produced without Mission Control, too. You can use jcmd to create JFR files, then copy them to desktop and open In Mission Control. Here is a quick instruction to capture JFR recording. You would need to know the PID of the target JVM and execute jcmd under the same user account. Start JFR recording with command below jcmd JFR.start settings=profile The result of this command should be something like what we have below. You would need to remember the number of the recording for later. Started recording 1. No limit (duration/maxsize/maxage) in use. Use JFR.dump recording=1 filename=FILEPATH to copy recording data to file. Wait a little for data to collect Use the command suggested in step 1 to dump JFR data jcmd JFR.dump recording= filename= This action would create a JFR file. Stop the recording session jcmd JFR.stop recording= Now that you have a JFR file, you can open in Mission Control later. Method profiling report As you may remember from the previous article, Flight Recorder collects various types of events and Mission Control builds a number of reports from that data. We want to identify areas of code that have contributed most to the execution time of our request. The usual profiling method for such tasks is a stack trace sampling. The idea is simple. During execution at regular intervals, Flight Recorder is recording trace for each thread. Taking samples from a single thread and observing that method X is present on a stack in 50% of the sample, you can assume that the sum of all method X invocation time is 50% of profiling session time. While the idea is simple, well, in practice, you have to deal with multiple threads and non-ideal sample distribution. Still, this approach remains extremely useful in practice. Let’s open the “Method profiling” report. The report itself is pretty barebone, and if you have used a different Java profiler before, it feels lacking details. They are hidden in a “Stack Trace” view (panel below). Power of “Stack Trace” view Stack view allows visualizing a set of stack traces as a tree. How do stack traces become a tree? We all know how Java stack traces look like (be it exception stack trace or stack trace from thread dump). Data produced by sampling profiling is just a bunch of stack traces. Here are steps on how this data is transformed into a tree. Convert each stack trace into a string by concatenating frames. Now that you have a multiset of strings, calculate each one’s occurrences and compile a histogram. You’ve got a histogram, a table with two columns “trace” and “count.” If you sorted this table and group by common prefix, it would become a tree. In the context menu, you would have the following “Stack view” customization options. Group Stacktraces From This option controls how frames are concatenated in step 1 of the algorithm above and what would be root nodes in the tree. Last Frame — aka the “hot methods” mode. Thread Root — aka the “call tree” mode. Distinguish Frames By This option controls which information is stripped from the frame description in step 1. Take note of the “Line Number” option. Line Numbers add clutter to the tree, but sometimes you want them to be visible to get the exact reference at source code. Layout Options The default tree is horizontally compressed, which is good but could confuse fresh eyes. Here you can switch back to a classic tree presentation. Stack view would work for all reports based on events that incorporate stack trace (including method profiling). Mission Control could even mix different types of events in the same stack view, but be aware that mixing events sampled by different rules (e.g., method profiling and IO traces) does not make much sense. Why CPU usage is important I mention what CPU metrics are important during profiling with JDK Flight Recorder. If we look at Java thread at any given time, it could be running on CPU in Java code ready to run in Java code but waiting in OS queue to get CPU running on CPU in native (non-Java) code ready to run in native code but waiting in OS queue to get CPU waiting to do something in native non-Java code waiting/blocked at the Java level JFR samples only threads in categories 1 or 2. There are also separate sampling for 3, 4 and 5, which is not well exposed in any dedicated report in Mission Control at the time of writing. So, if your server is starving on CPU, JFR will show you a particular picture, but it would be skewed due to mixing 1 and 2 categories. Sampling picture and would not accurately reflect real code computation cost in this situation. If the server is not CPU starved and threads you are profiling are actively consuming CPU, method sampling is the right tool for you to start with. If the server is not CPU starved and threads you are profiling are low on CPU, you are likely to have a “cold” hotspot, falling into categories 5 and 6. Category 5 code you can analyze with the “Native method sampling” event. Category 6 is either contention or IO, examined with separate reports. So by looking at CPU, you get an idea which report would be most useful next. Retrospective thread CPU usage in Mission Control What if you have a JFR recording and have no idea of CPU usage by threads at runtime? No problem, that data is recorded in a JFR file as well. You need to go to the “Event Browser” and find the “Thread CPU Load” event under the “Operating System” / “Processor” category. Here CPU usage for each thread is recorded with 10-second intervals. Remember that the percentage is taken from the total number of cores: in my screenshot, 25% would mean 100% of a single CPU core. JVM and host OS CPU usages are also recorded. Look under the “CPU Load” event type in the same category. Limit scope to a subset of threads Cool, now you can find CPU hungry threads, but the “Method Profiling” report shows all threads. Filtering by a subset of threads would be a good idea. Mission Control has very flexible and very nonobvious filtering features. We have to have only a subset of threads in the “Method Profiling” report. Here are the steps. Go to the “Threads” view Select one or more threads in a list of threads on the left Right click and choose “Store Selection” in the context menu Go back to “Method Profiling” In the filter combo select source “Threads Histogram Selection” and aspect “Thread Name is …” Now report includes events only for threads you have chosen. Any filter is shared across all reports, so do not forget to reset it then switching reports. What do I see in the “Stack Trace” view of the “Threads” report? While walking through the steps above, you may have noticed that the “Stack Trace” view is available in “Threads”. Moreover, it was reacting to selection in the thread list. How is the “Stack Trace” on the “Threads” report different from the “Method Profiling” report? Each report has a scope: a set of events (usually certain types of events) used to calculate the report. When you are working with report UI, a focused subset of events in scope is maintained behind the scene. A filter could narrow the scope, and this is what we did in the previous section. Any event may include the stack trace, and many do. The “Stack Trace” view listens to a focused set of events in an active report and visualizes all stack traces available. In “Method Profiling,” only method profiling samples are in scope. The “Threads” report includes almost every event bound to some thread (both sampled and non-sampled). So, while technically they could be aggregated together, it does make little sense. Nevertheless, you can use filters to fix this to make the “Threads” report show the same picture as “Method Profiling”. Go to “Event Browser” Find “Method Profiling Sample” is event types tree Right click on it and choose “Store Selection” in the context menu Go back to the “Threads” report In filter combo select source “Event Types Tree Selection” and aspect “Event Type is Method Profiling Sample” Now your “Stack Trace” would include only method profiling data, and you can change select threads without switching reports. Caveats of JFR method profiling The way JFR takes stack traces during profiling is precise. The majority of Java profilers use a JVM wide thread dump to get traces of all threads at once. A thread dump is a Stop the World operation, and doing it 100 times per second could be expensive. JFR manages to avoid Stop the World. Instead of stopping all threads at once, JFR stops them individually (using OS facilities) and captures stack traces from individual threads. Entering/leaving Stop the World state involves sophisticated protocol between application and system threads in JVM, and JFR skips this overhead altogether. Yet, this optimization introduces certain nuances data captured by JFR. Only stack traces ending up in Java code (not the native code) are recorded and visible in the “Method profiling” report. Thus, unless threads you are looking at were active CPU consumers, the picture in “Method profiling” would be skewed. Besides “Method Profiling Sample”, there is the “Method Profiling Sample Native” event type which is capturing threads in states 4, 5 and 6 (see the list mentioned earlier). However, this event type is available only in the “Event Browser” not in the “Method Profiling” or the “Threads” reports. The good news is that the “Stack Trace” view also works in the “Event Browser” report. Another important caveat for all types of Java sampling profilers is the infamous “safepoint bias.” In short, due to various effects of JVM runtime and JIT compiler, profilers could incorrectly identify the exact hot method or code line. In some edge cases, profilers could be hilariously inaccurate. Technically, JFR avoids the “safepoint” part of the problem, though it is still biased due to the way Java JIT compiler works. Here you can read more about a few examples of such hilarious profiler inaccuracy. While the edge case of the “safepoint bias” effect could make you lose your trust in the profiler, it is rarely a problem in actuality. A profiler’s job is to narrow the scope of search; it doesn’t need to be 100% accurate to be useful. But do not trust it blindly either. “Cold” code hotspots A hotspot is a portion of code (e.g., method or line of the source file) responsible for a considerable part of execution time during request processing (or other kinds of workload) related to other code involved. But how was that time spent? Was that code burning CPU cycles or just sitting cold off the CPU waiting for something? “Cold” hotspot is a kind of code that consumes time, but does not consume CPU resources. Most typical kinds of “cold” hotspots are Blocking IO calls Contention points in multi-threaded applications Waiting for completions of async tasks The method sampling approach used by JFR is not suitable for identifying “cold” host spots (it could see only the “CPU hot” code). There are separate reports for blocking IO and contention. But how would you know what you are dealing with a “cold” hot spot? Low thread CPU usage for a thread supposed to process requests is a symptom that should trigger your attention. File I/O report This report is composed of “File Read” and “File Write” events, capturing blocking file IO operations. Since events include file name, this report shows at the file level how many bytes were read/written to each file and how much time it has taken. An important caveat here is to consider the JFR recording session’s threshold; only operations exceeding it would be visible here. The report is relatively simple but could be quite powerful if combined with the filtering capabilities of Mission Control. Naturally, filtering and “Stack Trace” views are also available here. Socket I/O report The Socket I/O report is fairly similar to File I/O, but aggregation is shown by remote address and port. The taskbar lists three aggregation options available. By Host By Port By Host and Port Lock instances report Lock instances reports are based on the “Java Monitor Blocked” event type. These events are produced when a Java thread is blocking trying to acquire the semaphore. This report is only useful with “old style” synchronized keyword-based thread coordination (and by old I do not mean outdated, “synchronized” has its case in modern Java). What about “java.util.concurrent” based contention? JFR equally has events related to “new style” synchronization “Java Thread Park”, though there have been no dedicated reports in Mission Control so far. Threads report We’ve already had a brief interaction with the “Threads” report. It consists of a thread list and a timeline area. The thread list is simply a list of threads, but the timeline is more interesting. It has lanes for each thread and could visualize a wide range of JFR events. By default, they are so-called “Java Latency” events, but you can customize that via the context menu (see “Edit Thread Activity Lanes” option). Typically, individual events on the timeline would be tiny. You can hover to get one’s details under the mouse pointer, but it is not very useful if events are subpixel sized. You can zoom, though, drag the rectangle over the timeline and hit “Zoom to Selected Range” from the context menu to zoom in. You can use the time range selected in the “Threads” report as a filter on other reports, too. Conclusions In this article, I have shown key features of JDK Flight Recorder / Mission Control most useful to code execution profiling or hunting “code hotspots.” From its length, you could assume that Mission Control is not the most intuitive tool. This is true, and one should keep in mind a few fundamental principles to use it efficiently. Flight Recorder could be started for the command line, which is very helpful in environments where JMX access is impossible or complicated. In addition, it does not require any upfront JVM configuration; if your JDK supports JFR, you can start using it anytime. CPU usage is a crucial metric. It is essential you know which kind of hotspot you are looking for. If you cannot monitor CPU usage in real time, you can find this information in the Flight Recorder file. CPU consuming (“hot”) hotspots are very different from “cold” ones caused by code spending time in idle state. For a former traditional stack, trace sampling (the “Method Profiling” report) works fairly well. Later instances are more sophisticated. You may need to enable zero thresholds for I/O and contention events to fully picture idle state events in Mission Control. “Stack Trace” view is a potent tool you really need to get familiar with. It works for a wide range of events. For sampled events, you are most likely to be using “hot methods” mode, whereas “call tree” is more informative for traced ones. Performance is a hard topic. No matter how great your tools are, you need to learn JVM and OS’s inner workings. Yet tools are valuable and would save you lots of time. JDK Flight Recorder and Mission Control are a great combo, and them being completely open source is an additional advantage in my eyes. While the learning curve is steep, getting to know even a few tricks from this tool arsenal could help you greatly when dealing with performance issues in Java. Related Posts JDK Flight Recorder – a gem hidden in OpenJDK Hunting down memory issues with JDK Flight Recorder JVM in Linux containers, surviving the isolation JDK Flight Recorder, The Programmatic Way - [7 reasons to switch from Oracle JDK to OpenJDK](https://bell-sw.com/announcements/2020/08/14/7-reasons-to-switch-from-Oracle-JDK-to-Liberica-JDK/): It seems that Oracle developed a habit of changing licensing conditions for Oracle JDK every two years: In January 2019, it stopped releasing free builds of Oracle Java SE 8 for commercial use; In 2021, the company announced a change in its licensing policy with the release of JDK 17. LTS-releases are now released every two years and according to a new license, receive free updates for 3 years. After that, enterprises have to migrate to the next LTS version or pay for commercial support; In 2023, the vendor overturned the pricing model completely by introducing the new “Employee for Java SE Universal Subscription” metric. According to it, the number of licenses is calculated based on the number of employees, including full-time, part-time, temporary employees and employees of agents, contractors, outsourcers, and consultants that support internal business operations. If you have stumbled upon this article, chances are that your enterprise considers migrating from Oracle JDK. In the upcoming paragraphs, we will compare Oracle JDK and OpenJDK so that you can decide whether migration will be beneficial for your project. You can also read more about the OpenJDK project in our What is OpenJDK article. Table of Contents What is Oracle Java? Are OpenJDK and Oracle JDK the same? What is the difference between Oracle JDK and OpenJDK? Is it difficult to migrate from Oracle to OpenJDK? Is OpenJDK secure? Is OpenJDK performant? Can you use OpenJDK for enterprise development? Can you use OpenJDK for closed-source applications? Does OpenJDK provide all the features Oracle JDK has? Comparison of OpenJDK and Oracle JDK Liberica JDK — OpenJDK distribution for modern Java for modern Java deployments 1. Liberica JDK provides the most complete Java experience 2. Liberica JDK is affordable 3. Liberica JDK is safe 4. Liberica JDK offers JDK Flight Recorder + Mission Control for free 5. Liberica JDK is easy to install 6. Liberica JDK is compliant with standards 7. Liberica JDK is in touch with the community Customer success stories VMware Tanzu OOCL JetBrains Flow Traders Have we convinced you to switch from Oracle JDK? What is Oracle Java? Oracle Java (or Oracle JDK) is a set of computer software and specifications initially developed at Sun Microsystems and then acquired by Oracle. Oracle Java rapidly gained popularity in various sectors, such as banking and fintech, e-commerce, game development, etc. In 2006, the OpenJDK project was born following the promise of Sun Microsystems to make Java code open-source. When Oracle stopped releasing free builds of Oracle Java SE 8 for commercial use and changed Java licensing policy, many enterprises turned to OpenJDK as a reliable and free alternative to Oracle Java. Are OpenJDK and Oracle JDK the same? Both Oracle JDK and OpenJDK are developed on the same code base, so the technological foundation is the same. But for many people, Oracle means a “prominent trustworthy company,” which is true, while OpenJDK means “some community trying their best making open source Java runtime” — and here is where they are wrong. Take BellSoft, one of the major OpenJDK enterprise contributors: before launching the company, its founders and engineers have worked at Oracle for years. They know Java™ on multiple levels like they know their own mind and do everything to meet the standards. Liberica binaries have passed all the Technology Compatibility Kit (TCK) tests for Java provided by Oracle and are fully ratified in the Java Community Process. What is the difference between Oracle JDK and OpenJDK? The pivotal difference between Oracle JDK and OpenJDK is licensing. OpenJDK is an open-source project licensed under GPLv2 + Classpath exception. On the other hand, Oracle Java is closed source, and enterprises need to purchase commercial licenses to receive Java updates. Oracle JDK versions are available under various licenses: Starting with Oracle JDK 17, the builds are available under the Oracle No-Fee Terms and Conditions license; Oracle JDK 11 and 8 are available via My Oracle support or under the OTN license that permits personal use. You can read more on terms and conditions of mentioned licenses in our article dedicated to Java licensing. Other differences are associated with additional functionality and supported platforms. For instance, Oracle Java doesn’t include Shenandoah GC, but it is shipped with Liberica JDK and other major OpenJDK distributions. A comprehensive comparison of features offered by the most popular OpenJDK distributions and Oracle Java can be found in the Oracle Java alternatives article. Is it difficult to migrate from Oracle to OpenJDK? No, it isn’t. Both are implementations of the same Java specification, so switching requires little to no adjustments. Your engineering team could pick a Liberica JDK container image right now, literally change one line of code, and use the base distribution without issues. Is OpenJDK secure? The latest OpenJDK releases are immensely safer than outdated releases from Oracle with a permissive license. The OpenJDK community members, both companies and individual developers, constantly hunt down vulnerabilities and bugs and promptly fix them. For instance, BellSoft signed the OCTLA and became a part of the Vulnerability Group alongside other major distributors such as Oracle, SAP, Amazon, and Red Hat. Together they search for, review, and patch security issues. Is OpenJDK performant? From the JVM performance point, it has no differences from Oracle JDK. Moreover, the performance equality is confirmed by major industry-standard benchmarks (SPECjbb, SPECjvm) and microbenchmarks embedded in the OpenJDK code. Can you use OpenJDK for enterprise development? OpenJDK is an optimal choice for enterprise development. As we stated earlier, it is performant, reliable, and secure. With OpenJDK, you are able to use the runtime without any vendor participation. At the same time, it is possible to acquire commercial support from an OpenJDK vendor at much more affordable prices than from Oracle. If you choose to work with a vendor, it is good to have a clear idea of which company is the supplier of your underlying technology to avoid vendor lock-in. BellSoft guarantees no lock-in by providing builds of vanilla OpenJDK and contributing all the fixes upstream. Can you use OpenJDK for closed-source applications? Copyleft licenses, such as the GNU General Public License (GPL), are considered viral. Any code that uses a GPL library automatically becomes GPL as well. Contrary to popular belief, OpenJDK does not mean that anything linking to it needs to be distributed under the GPL terms. The license for OpenJDK is not just “GPL v2”, it is “GPL v2 with the Classpath Exception.” It allows linking OpenJDK with any independent module regardless of GPL terms, copying and distributing the resulting files under terms of your choice. Obviously, this independent module can be your proprietary code, which means that you may utilize the GPL components of OpenJDK while maintaining the integrity of your intellectual property. Your products thus may be copyrighted under a different license. Does OpenJDK provide all the features Oracle JDK has? If we talk about most versions after Java 8, the current OpenJDK and Oracle JDK releases show no differences. However, version 8 has certain technologies absent in OpenJDK (that are gradually being backported). The table below shows all the qualities and features that distinguish Liberica JDK from Oracle JDK with some alternative tools that may replace the original Oracle JDK 8 components. Comparison of OpenJDK and Oracle JDK The table below demonstrates the difference between OpenJDK and Oracle Java using Liberica JDK as an example. Liberica JDK Oracle JDK License, Security, Compatibility, and Sources Based on OpenJDK Yes Yes Open-source license, no field of use restrictions Yes Versions up to JDK 17 are under OTN. Starting with JDK 17, the license is NFTC Quarterly Security Updates Yes Yes Compliance with Java SE specification guaranteed by TCK Yes Yes Performance Parity with Oracle Java SE Yes Yes Guaranteed LTS (8, 11) and Feature (12, 13, 14) versions Yes Yes Platforms & Installers Windows x86 (64 bit) Yes Yes Windows x86 (32 bit) Yes JDK 8 only macOS Yes Yes Linux x86 (64 bit) Yes Yes Linux x86 (32 bit) Yes JDK 8 only Alpine Linux x86 (64 bit, musl) Yes Yes Linux ARM (32 bit) Yes JDK 8 only, Embedded license Linux ARM (64 bit) Yes JDK 8 only Solaris Sparc JDK 8, 11 JDK 8, 11 Solaris x86 (64 bit) JDK 8, 11 JDK 8 only Windows Installers MSI EXE Mac Installers DMG, PKG DMG Linux Installers DEB, RPM, TAR.GZ DEB, RPM, TAR.GZ Variety of bundle types Lite, Standard, Full, JDK/JRE Standard, Server JRE/JDK Linux Repositories (yum, apt) Yes No Product Features JDK Flight Recorder JDK 8+ JDK 8, JDK 11 Java Mission Control Yes Yes OpenJFX (JavaFX) JDK 8, 11, 17 JDK 8 only Java Web Start and Applets JWS is supported in the Commercial Bundle with OpenWebStart from Karakun Yes Graphics Renderer Marlin in JDK 8, 11 Ductus in JDK 8, Marlin in JDK 11 Font rendering engine Freetype in JDK 8, 11 T2K in JDK 8, Freetype in JDK 11 Commercial Support Provided for Supported Platforms & Environments Yes Yes 24x7x365 Support (1-hour SLA) Yes Yes SLA for Quarterly Updates Yes Yes Out-of-cycle Critical Fixes (independent from OpenJDK) Yes Yes Commercial Support Production Lifecycle 2031 for Java SE 2026 for JavaFX 2030 for Java SE 2025 for JavaFX Engineering capacity to root-cause and fix bugs (independent from OpenJDK) Yes Yes As you can see, migration to OpenJDK doesn’t pose any security or compatibility risks. On the contrary, it will enable you to eliminate vendor lock-in and optimize software expenses. The next big question is: which OpenJDK distribution to choose? Liberica JDK — OpenJDK distribution for modern Java for modern Java deployments 1. Liberica JDK provides the most complete Java experience Migration to Liberica JDK enables you to cover all your Java needs: The Liberica JDK binaries are supported on most existing platforms and system configurations; BellSoft supports legacy Java versions 6 & 7, all LTS versions (8, 11, 17), feature releases, and GraalVM; We developed a special Liberica JDK Lite version optimized for cloud deployments, which helps to minimize resource consumption; You can utilize Liberica JDK for Embedded for systems with lower performance. In addition, to Liberica JDK you gain access to other useful Java utilities: Convert Java apps into native images with accelerated startup using our GraalVM-based Liberica Native Image Kit. It is utilized by default with Cloud Native Buildpacks and recommended by Spring as a Native Build tool; Control and update all corporate Java runtimes from one window with Liberica Administration Center; But there is more! In 2022, our engineers developed Alpaquita Linux, a lightweight distro that comes with two libc implementations (optimized musl and glibc) and Java tools facilitating the development and deployment of Java applications. Coupled with Alpaquita, Liberica JDK enables you to build the smallest containers on the market that can take up as little as 38 MB! And if you seek higher availability and predictable scalability in the cloud, Liberica JDK supports Coordinated Restore at Checkpoint API that gives your Java apps almost instant startup at peak performance and allows for optimized resource consumption. Follow our guide on using Java with CRaC in a container and give this feature a try. As a result, by switching from Oracle to Liberica JDK, you save on support, but at the same time create a full software stack tailored to your needs. 2. Liberica JDK is affordable You may use our binaries for free. But if you would like to have a reliable partner who is there for you 24/7, and gives feedback on your query within 24 hours based on SLA, BellSoft offers versatile support plans with per-instance pay which are much less costly than Oracle’s, considering that Oracle counts employees, not equipment. For example, you have 50 servers and 250 employees. You will pay $15,000 per year for Liberica JDK or $45,000 per year for Oracle Java. In case you have 500 servers and 2,500 employees, the annual subscription for Liberica JDK will be $70,000 and for Oracle Java — $360,000. The calculation is based on the prices effective in November 2022 and presented on corresponding Oracle Java SE Universal Subscription Global Price List and Liberica JDK support price pages. 3. Liberica JDK is safe We provide continuous updates for the current and LTS versions with bug fixes and security patches as part of the CPU (critical patch update) quarterly release cycle. Commercial customers also receive off-cycle and extra patches not yet included into the OpenJDK code. 4. Liberica JDK offers JDK Flight Recorder + Mission Control for free These indispensable tools were first introduced in Oracle Java 7, along with other features that required licensing and subscription. Since the release of JDK 8u262, everyone can use them in all Liberica versions unconditionally. 5. Liberica JDK is easy to install We have more channels to deliver Liberica JDK to our customers than any other vendor. Starting with Java 17, Oracle JDK is released under a new “Oracle No-Fee Terms and Conditions” (NFTC) License, where sanction restrictions are explicitly mentioned. New Oracle JDK builds are provided on the official website of the company. Previous Oracle JDK versions are still licensed under Oracle Technical Network (OTN), and OTN serves as a centralized repository for registered Oracle users. BellSoft offers a broad choice: Pick a container image on Docker Hub; Download a Windows auto-installer; Install a repository for Linux; Find builds on package managers; Download binaries directly from the website or through our public API. Not to mention early access builds that we provide for all update releases so that our clients could test them beforehand. Note that the build with the JFR port for JDK 8 is included into release builds of Liberica JDK and builds of other OpenJDK vendors with HotSpot. However, Oracle has a closed JFR implementation, this function is provided with a flag on a commercial basis. In addition, if you use buildpacks to automate the containerization process, Liberica JDK is the default JVM in Paketo buildpacks. 6. Liberica JDK is compliant with standards We believe in quality level testing for our customers’ peace of mind. All produced binaries are verified by TCK for Java SE specifications. Moreover, Liberica JDK for macOS is a notarized product, which means you can refer to it when developing your applications on this platform. 7. Liberica JDK is in touch with the community Being among the major OpenJDK contributors, we make sure that any bug we fix for a client during troubleshooting is immediately corrected for everyone. We do not have a downstream repository, so our fixes get integrated into the JDK mainline branch as fast as possible, unlike some other vendors that work towards eventual consistency with OpenJDK. To learn more about Liberica JDK technical characteristics and use cases, download the Product Summary by clicking the button below. Download Liberica JDK white paper Customer success stories Millions of users all over the world already use Liberica JDK. The majority of our clients have traveled the same path you are going to and migrated from Oracle JDK to Liberica. Let’s see what they say about their digital transformation and overall experience with BellSoft. VMware Tanzu VMware is a leading provider of multi-cloud services for all applications. Over the years, BellSoft has supported VMware’s JDK development efforts by providing Liberica JDK. Satisfied with timely and reliable support, VMware invited BellSoft to extend the relationship by providing compiler and Java Runtime support for Spring Native — a recent addition to the Spring Framework ecosystem for compiling Spring Boot applications into native executables. VMware now provides their users with both native support for Spring and Liberica Native Image Kit through the Native Image Buildpack. OOCL OOCL is one of the world's largest integrated international container transportation and logistics companies. OOCL provides transportation services in Asia, Europe, the Americas, Africa and Australasia. Liberica JDK delivers the stable and secure Java environment that helps OOCL to provide its customers with fully-integrated logistics. JetBrains We asked Konstantin Bulenkov, JetBrains Runtime Lead: Why did you choose BellSoft as the partner to support JetBrains Runtime? “When Apple made the decision to discontinue support for JDK on macOS, we ended up facing a major problem with respect to text rendering quality in the IntelliJ editor. So we decided to start the JetBrains Runtime project. The biggest fear we had before making the switch to our own runtime was security. And Oracle’s move last year to stop providing public updates for JDK 8 forced us to work on finding a way to make JetBrains Runtime more secure. We realized that Liberica JDK is a 100% open source project, and BellSoft is one of the leading OpenJDK contributors. BellSoft, part of OpenJDK Vulnerability Group, collaborates with other contributors to keep OpenJDK robust. JetBrains relies on the Liberica team’s experience and expertise to provide timely updates for our customers; together, we keep JetBrains Runtime secure and performant.” Have we convinced you to switch from Oracle JDK? Leave your details by clicking the button below, consult with BellSoft’s professional team and start your migration now! Flow Traders We asked Yury Vasyutinskiy, Team Lead at Flow Traders: What were the essential aspects you sought in a Java runtime vendor? “Flow Traders is a leading global financial technology-enabled liquidity provider in financial products, historically specialized in Exchange Traded Products (ETPs), now expanding into other asset classes. Flow Traders ensures the provision of liquidity to support the uninterrupted functioning of financial markets. It allows investors to continue to buy or sell ETPs or other financial instruments under all market circumstances. We continuously grow our organization, ensuring that our trading desks in Europe, the Americas and Asia can provide liquidity across all major exchanges, globally, 24 hours a day. Technology is a key component in enabling this growth; therefore, we aim to be in as much control as possible to our trading platform. Thus, we develop and maintain a large code base with dedicated in-house resources. Due to the change in the licensing model for Oracle JDK, we have decided to look around for other JDK vendors. We embrace innovation and new technologies, as we believe that helps us to step up our game. But we stay pragmatic and check all the details thoroughly. Hence several requirements were identified for switching to a new vendor solution: Stability of the distribution; Clear SLA on security patches delivery; Full compatibility with our landscape; Performance of the distribution. At Flow Traders, we invest significantly in the quality of our products. Being one of the leading global ETP liquidity providers, we have extensive quality control systems that cover both functional and non-functional aspects of our products. We have conducted a series of different tests to check all the requirements. As a result, Liberica JDK was chosen as our main JDK distribution. We are satisfied with the Liberica JDK as we managed to onboard it without any major hiccups. We are also very happy with the level of support provided by the Bellsoft team and are looking forward to strengthening our relationship in the coming years.” Have we convinced you to switch from Oracle JDK? Leave your details by clicking the button below, consult with BellSoft’s professional team and start your migration now! Contact us! - [Hunting down memory issues with JDK Flight Recorder](https://bell-sw.com/announcements/2020/09/02/Hunting-down-memory-issues-with-JDK-Flight-Recorder/): In this post about JDK Flight Recorder and Mission Control, a powerful diagnostic and profiling tool built into OpenJDK, I would like to focus on JVM memory features. What are the memory problems? If you put the words “garbage collection” and “problem” into one sentence, many would think of the infamous Stop-the-World pauses that JVM is prone to trigger. Lengthy Stop-the-World pauses are indeed very problematic from an application perspective. While garbage collection (GC) and Stop-the-World (STW) pauses are connected, they are not the same. The problem with GC is likely to manifest itself as prolonged STW pauses, but GC does not necessarily cause them. General optimization of application code allocation patterns is another standard task, and Flight Recorder with Mission Control could be of great help here. Optimized memory allocation in application code suits both for GC and overall execution performance. Finally, the application code may have memory leaks. Though I believe heap dumps are the best tool for diagnosing memory leaks, JDK Flight Recorder offers an alternative approach with its pros and cons. In particular, heap dumps require Stop-the-World heap inspection to capture and tend to be extensive (depending on the heap size), while JFR files do not grow proportionally to the heap size. Below is a plan for the rest of this article. Garbage collection related reports JVM Operations aka the Stop-the-World pause report Memory allocation sampling Old object sampling overview Starting memory profiling JDK Flight Recorder session You would need OpenJDK with JDK Flight Recorder support and Mission Control. I was using Liberica OpenJDK 11.0.7+10 and Liberica Mission Control 7.1.1. There are multiple ways to start a JDK Flight Recorder session. You can do it with Mission Control UI (locally or via JMX), with jcmd from console, or even configure it to auto-start with JVM command-line arguments. All these options are covered in my previous post, so I will omit the details here and jump right to Flight Recorder session settings. The configuration dialog of the Flight Recorder session in Mission Control has a few options relevant to the features I am going to explain later. Garbage Collector The default level is “Normal,” and it should be enough. You may choose “All,” which adds more details about GC Phases. Memory Profiling Here you may choose one of three options: Off Object Allocation and Promotion All, including Heap Statistics I strongly suggest you stay with “Object Allocation and Promotion.” It provides valuable data points for allocation profiling with low overhead. Heap Statistics included in the last option force regular Stop-the-World heap inspections which could be quite lengthy. Memory Leak Detection If you suspect memory leaks in your application, tweak these options. You need to choose the “Object Types + Allocation Stack Traces + Path to GC Root” level of details to see the leaking path in the report afterward. Calculating “Path to GC Root” is a moderately expensive Stop-the-World operation, and I would not recommend enabling it just as a precaution. Garbage collection in OpenJDK OpenJDK JVM can use different implementations of garbage collectors. The most commonly used ones in HotSpot JVM are Garbage First Parallel Mark Sweep Compact Concurrent Mark Sweep (CMS) — discontinued in Java 14 There are several other algorithms available in various JDKs, but I am not going to get too deep for the sake of brevity. Each GC has its unique features, but a few are the same. All the algorithms above are generational, meaning they split available heap space as separate young and old space. The result is three types of GC cycle: Minor (young) collections — recycle only young space Major (old) collections — recycle old and young space Last resort full collections — a big STW whole heap collection you want to avoid Each GC cycle is split into a hierarchy of phases. Some could be concurrent (executing in parallel with application code), but most require a Stop-the-World pause. Garbage collector report Mission Control has a generic “Garbage Collections” report agnostic of the GC algorithm used by JVM. This report shows a list of GC pauses with break down by phases (table on the right). Information in this report could replace the typical usage of GC logs. Here you can quickly spot the longest GC pause and dig a little deeper into its details. Notice GC ID assigned to each GC cycle: they are useful to pick additional events related to GC but not included in this report. Two key metrics could be used to assess GC performance regardless of the algorithm: Max duration of a Stop-the-World pause — it may add up to your application’s request processing time, hence increasing response time. Percentage of time spent in a Stop-the-World pause during a certain period — e.g., if we spent 6 seconds per minute in GC, the application can utilize 90% of available CPU resources at most. The chart on the “Garbage collection” report helps to spot both metrics easily. “Longest pause” and “Sum of pauses” visualize these metrics aggregated over regular time buckets (2 minutes in the screenshot). “Longest pause” shows the longest GC pause in the bucket, so you can observe your regular long pauses and outliers, if applicable. “Sum of pauses” shows the sum of all GC pauses in the bucket. Use this metric to derive the percentage of time spent on GC pauses. Hover your mouse over the bar, and you will see the exact numbers in the tooltip. In the example screenshot, the sum of pauses over 2 minutes equals 4 seconds. GC overhead is 4 / 120 ≈ 3.3%, which is reasonable. What if you are not happy with your GC performance? GC tuning is a big topic out of scope for this article, but I will highlight a few typical cases. ‘Heap is too small’ problem GC in modern JVM usually just works. A lot of engineering effort has gone into making GC adaptable for the application needs. Although GC is not magic, it needs enough memory to accommodate application live data sets and some head room to work efficiently. If the heap size is not enough, you will see increased GC overhead (percentage of time spent on GC pauses) and, as a consequence, the application slowing down. The screenshot above illustrates JVM running on low memory. During the hovered time bucket, 1.4 seconds out of 2, JVM was in a GC pause, which is an absolutely unhealthy proportion. Often increasing the max heap allowed to JVM solves the problem with high GC overhead. However, if an application has a memory leak, increasing heap size is no help. The leak in such a situation needs to be fixed. ‘Metaspace is too small’ problem Metaspace is a memory area to store class metadata. Starting from Java SE 8, metaspace has not been part of heap, even though the lack of available space in metaspace may cause GC there. To free some memory in metaspace, JVM needs to unload unused classes. Class unloading, in turn, requires a major GC on the heap. The reason is that objects on the heap keep references to corresponding classes, and a class cannot be unloaded until any instance of it exists in the heap. You would quickly spot the metaspace keyword in the “Cause” column in this case. You can also view the metaspace size in the timeline chart at the bottom of the report to check if you are hitting the limit. Special reference abuse Java™ offers utility classes that have very specific semantics related to garbage collection. These are weak, soft and phantom references. Besides, JVM is using finalizer references internally to implement the semantics of finalizers. All these references require particular processing within the bounds of a GC pause. Due to semantic special references, they can only be processed after all live objects are found. Usually, this processing runs fast, but abusing these special references (especially finalizers as they are most expensive) could cause abnormally long pauses for otherwise healthy GC. Columns with a count of each special reference type are on the right side in the GC event table. Numbers on the order of tens of thousands could indicate a problem. You can also find the reference processing phase in the “Pause Phases” table and see how much time was spent on it. More GC related events The “Garbage Collections” report is a handy tool to spot standard problems, but it does not display all GC details available from JDK Flight Recorder. As usual, you can get to the event browser and look at specific events directly. Look under Java Virtual Machine / GC / Details for more diagnostic data related to garbage collection. Besides work done under Stop-the-World, some collectors (G1 in particular) use background threads working in parallel with application threads to assist in memory reclamation. You may find details for such tasks if you look at the “GC Phase Concurrent” event type. Stop-the-World pauses in OpenJDK A Stop-the-World (STW) pause is a state of JVM when all application threads are frozen, and internal JVM routines have exclusive access to the process memory. Hotspot JVM has a protocol called safepoint to ensure proper Stop-the-World pauses. STW pauses are mainly associated with GC activity, but it is but one possible reason. In Hotspot JVM, STW pauses get involved in other special JVM operations. Namely: JIT compiler related operations (e.g. deoptimization or OSR) Bias lock revocation Thread dumps and other diagnostic operations, including JFR-specific ones As a rule, STW pauses are unnoticeably short (less than a millisecond), but putting JVM on a safepoint is a sophisticated process. Things can go wrong here. In the previous post, I mentioned the safepoint bias. Safepoint protocol requires each thread executing Java code to explicitly put itself in a “safe” state, where JVM knows exactly how local variables are stored in the memory on the stack. After it starts, each thread running Java code receives a signal to put itself on the safepoint. A JVM operation cannot begin until all threads have confirmed their safe state (threads executing native code via JNI are an exception; STW does not stop them unless they try to access heap to switch from native code to Java). If one or more threads are slow at reaching the safe state, the rest of the JVM will wait for them to be frozen effectively. There are two main reasons for such misbehavior: A thread is blocked while accessing a memory page (d state in Linux) and cannot react A thread is stuck in a hot loop, where the JIT compiler omitted a safepoint check as it was considered too fast of a loop. The first situation may happen if the system is swapping or application code is using memory-mapped IO. The second one may occur in heavily optimized computation-intensive code (in Java runtime, java.lang.Sting and java.util.BigDecimal operations over large objects are common culprits). VM Operations report Mission Control has a report with a summary of all Stop-The-World VM operations. This report shows a summary grouped by type of operations. In the screenshot, “CGC_Operation” and “G1CollectForAllocation” are the only operations related to GC (G1 in particular). You can see a fair number of other VM operations, but they are very quick. Also, bear in mind that many operations are caused by JFR itself, such as “JFRCheckpoint,” “JFROldObject,” “FindDeadlocks,” etc. Problems with safepoints are relatively rare in practice, but spending a few seconds to quickly check the “VM Operations” report for abnormal pauses is a good habit. Memory allocation profiling While JVM is heavily optimized for allocating a lot of short-lived objects, excessive memory allocation may cause various problems. Allocation profiling helps to identify which code is responsible for the most intense allocation of new objects. Mission control has a “Memory” report giving an overview of object allocation in application code. This report shows a timeline of the application’s heap allocation rate and a histogram of allocation size by object type. At first glance, the report looks too ascetic and done in broad strokes, notably if you have used Mission Control 5.5. There are many more details to squeeze out from this report with Mission Control magic, but we need to get an idea of the source for these numbers. How are allocation profiling data collected? Allocation profiling in JVM was a rather challenging task. While previously many profilers had allocation profiling features, the performance impact on an application with allocation profiling turned on was inconvenient and often prohibitive. Allocation profiling is based on sampling. A runtime allocation profiler collects a sample of allocation events to get the whole picture. Before JEP-331 (available since OpenJDK 11), profilers had to instrument all Java code and inject extra logic at every allocation site. The performance impact from such code mangling was dramatic. JDK Flight Recorder, on the contrary, was always able to use low overhead allocation profiling. The key is to record TLAB allocation events instead of sampling normal ones. Almost all new Java objects are allocated in the so-called Eden (a part of young object heap space). Not to compete for shared memory management structures, each Java thread reserves a thread local allocation buffer (TLAB) in Eden and allocates new objects there. It is an essential performance optimization for multicore hardware. Eventually, the buffer dries up, and the thread has to allocate a new one. This type of event is the one recorded by Flight Recorder. The average TLAB size is roughly 500KiB (size is dynamic and changes per thread), so one allocation per ~500 KiB of allocated memory is recorded. This way, Flight Recorder does not add any overhead to the hot path of object allocation, yet it can get enough samples for further analysis. Each sample has a reference to the Java thread, stack trace, type and size of the object being allocated, as well as the size of the new TLAB. You can find raw events in the event browser under “Allocation in new TLAB.” But how would I know which code is allocating the most? The “Memory” report is based on the “Allocation in new TLAB” events, which has associated thread and stack trace. It means we can use the “Stack Trace” view and filtering to extract much more details from this report. Typical workflow for identifying memory hot spot would look as follows: Open the “Memory” report. Open the “Stack Trace” view (enable via Window > Show View > Stack Trace if hidden). Enable “Show as Tree” and “Group traces from last method” on the “Stack Trace” view. Such a configuration is the most convenient for this kind of sampling. Switch “Distinguish Frames By” to “Line Number” in the context menu of the “Stack Trace” view if you want to see line numbers. By this point, you can already see a top hot spot of allocation in your application. You may also select a specific object type in the “Memory” report to only examine these type-related allocation traces. Further, you can select a range of timelines to zoom in on a definite period. Another functionality here is applying filters to narrow the report to individual threads. All the tricks for the “Method Profiling” report described in the previous post would work here too. Live object sampling Memory leaks are another well-known problem for Java applications. Traditional approaches to memory leak diagnostics rely on heap dumps. While I consider heap dumps a vital tool for dealing with memory troubleshooting optimizations, they have their drawbacks. More specifically, heap dumps could be quite large, slow to process and require Stop-the-World heap inspection to capture. Flight Recorders offer an alternative approach, live object sampling. The idea is simple: we already have allocation sampling. Now from the newly allocated objects, Flight Recorder picks few to trace across their lifetime. If an object dies, it is excluded from the sample (and no event is produced). Then events are dumped into a file at the end of recording. Flight Recorder may optionally calculate and record the path to the nearest GC root for each object in a live object sample (a configuration option mentioned at the beginning of this article). This operation is expensive—requiring an STW heap inspection—but necessary to understand how an object is leaking. But these are just random objects. How would sampling help find a leak? Normal objects are short-lived, but leaked objects could survive for long. If sampling JVM for a long enough time, you will likely catch a few leaked objects in the sample. Once these objects get to the final report, we can identify where they are leaking via the path to the nearest GC root, also recorded. Mission Control has a “Live Objects” report to visualize this kind of event. You would also like the “Stack Trace” view to be enabled here. In the table, you can see a sampled object grouped by GC root. You can unfold a reference path from a GC root down to individual objects from the sample. What is great is that you can see the allocation stack trace of each sampled object in the “Stack Trace” view as well. This information is unique to Flight Recorder and cannot be reconstructed from a regular heap dump. Does it work? This feature is relatively new in Flight Recorder, whereas heap dump analysis is a reliable, time-tested technique. A live object sample in Flight Recorder is rather small, and it is a matter of luck whether it would catch a problematic object or not. On the other hand, Flight Recorder files several orders smaller in magnitude compared to heap dumps. While Old Object Sampling is definitely not a silver bullet, it offers a fascinating alternative to the traditional heap dump wrangling approach for memory leak investigations. Conclusions Memory in JVM is a complicated matter. JDK Flight Recorder and Mission Control have many features to help users with typical problems. Nonetheless, the learning curve remains steep. On the bright side, complex JVM machinery such as GC and safepoints usually works fine on its own, so there is rarely a need to dig deep. Still, it is wise to know where to look if you have to. Memory optimization is a regular task. The allocation profiling features in Flight Recorder and Mission Control are highly beneficial in practice. If you asked me about the single most crucial Flight Recorder component, I would name allocation profiling without a second thought. Live object sampling is also an appealing approach for memory leak detection. Unlike heap dumps, it is much more appropriate for automation and continuous usage in production environments. Related Posts JDK Flight Recorder – a gem hidden in OpenJDK Hunting down code hotspots with JDK Flight Recorder JVM in Linux containers, surviving the isolation JDK Flight Recorder, The Programmatic Way - [Liberica JDK 15 brings efficiency and flexibility](https://bell-sw.com/announcements/2020/09/17/Liberica-JDK-15-brings-efficiency-and-flexibility/): It’s time to welcome the much-awaited version 15 of OpenJDK. The new release has 14 new features, 2949 bug fixes and backports in total, with eight issues resolved by the BellSoft team (like Thread-local handshakes implemented for Arm32). Let’s discuss the highlights introduced in JDK 15. 1. Production-Ready ZGC (JEP 377) and Shenandoah GC (JEP 379) Both these garbage collectors have been changed from experimental features to product features. They are low-latency GCs. ZGC is scalable, first integrated to JDK 11; Shenandoah GC is low-time-pause and appeared in JDK 12. The collectors attempt to tackle the common problem of too long stop-the-world pauses by using colored (Z) or Brooks (Shenandoah) object pointers for efficient concurrent work. They are enabled via -XX:+UseZGC and -XX:+UseShenandoahGC command-line options, respectively. ZGC and Shenandoah GC are supported on Linux x86_64, AArch64, Windows, and macOS. Shenandoah GC has been backported to JDK 11 and will become available in version 11.0.9 mid-October. G1 will remain the default garbage collector for all JDK releases starting from version 9. 2. Text Blocks (JEP 378) A text block is a multi-line string literal that prevents the need for most escape sequences, formats the string automatically and in an easy to predict manner. It also enables the developer to have control over the format, if they so desire. This functionality was first proposed in early 2019, targeted for JDK 12, but did not make it to the release. As a preview feature in versions 13 and 14, text blocks have changed their form a lot since. Starting from JDK 15, writing Java applications becomes ever more comfortable as these handy strings can be used to express several lines of source code and denote code written in other languages, increasing its readability. Text blocks are an excellent example of Java™ constantly evolving and a reminder for developers to use experimental features with caution, as they might end up far different from the first instance. Below are examples of text blocks in different languages. Scala: object ScalaTextBlock { def main(args: Array[String]): Unit = { val s = """def printHello(): Unit = { | println("Hello Text Block!") |} """.stripMargin println(s) } } /* Output: def printHello(): Unit = { println("Hello Text Block!") } */ Kotlin: fun main() { val source = """ fun printHello() = println("Hello Text Block!") """.trimIndent() println(source) } /* Output: fun printHello() = println("Hello Text Block!") */ Java: public class TextBlock { public static void main(String[] args) { var source = """ public void printHello() { System.out.println("Hello Text Block!"); } """; System.out.println(source); } } /* Output: public void printHello() { System.out.println("Hello Text Block!"); } */ 3. Edwards-Curve Digital Signature Algorithm (JEP 339) JDK 15 allows implementing cryptographic signatures based on the Edwards-Curve Digital Signature Algorithm (EdDSA). It is a modern elliptic curve signature scheme showing substantial benefits if compared to the other JDK signature schemes. For instance, at the same security level, EdDSA’s performance should improve over the Elliptic Curve Digital Signature Algorithm (ECDSA) based on native C code. It is the reason why EdDSA is rather popular and already supported in crypto libraries (OpenSSL, BoringSSL and many more). However, it will only be implemented in the SunEC provider, which would receive new Signature, KeyFactory, and KeyPairGenerator services. One of the upcoming JEPs will work on integrating EdDSA with JSSE for TLS 1.3. 4. Hidden Classes (JEP 371) Hidden classes cannot be used directly by the bytecode of other classes. They are meant to be used through reflection, indirectly, by frameworks that generate classes at runtime. Such a class may be defined as a member belonging to an access control nest (introduced by JEP 181) and unloaded independently of others. Enabling standard APIs to define not discoverable hidden classes with a limited lifecycle allows frameworks inside and outside the JDK to generate classes that could instead define hidden classes dynamically. Since many JVM languages, such as Clojure and Groovy, rely on dynamic class generation to be flexible, this feature will improve the efficiency of every programming language implementation built on the JVM. Hidden classes serve as a replacement for sun.misc.Unsafe::defineAnonymousClass, although they do not support its full functionality (such as constant-pool patching). The goal is to deprecate this non-standard API for complete removal in a future release. An example of a hidden class: import java.lang.invoke.MethodHandles; import static be.ClassLoad.getGeneratedClassBytes; import static java.lang.invoke.MethodHandles.Lookup.ClassOption.NESTMATE; import static java.lang.invoke.MethodHandles.lookup; public class HiddenClassExample { public static void main(String[] args) throws Exception { byte[] bytes = getGeneratedClassBytes(); MethodHandles.Lookup lookup = lookup() .defineHiddenClass(bytes, false, NESTMATE); Class hiddenClass = lookup.lookupClass(); System.out.println("canonical name = " + hiddenClass.getCanonicalName()); System.out.println("name = " + hiddenClass.getName()); System.out.println("isHidden = " + hiddenClass.isHidden()); Object instance = hiddenClass.getConstructors()[0].newInstance(); System.out.println("toString = " + instance.toString()); } } /* Output: canonical name = null name = HiddenClass/0x0000000800bb1c40 isHidden = true toString = This is hidden class! */ 5. Remove the Solaris and SPARC Ports (JEP 381) Given that many OpenJDK projects (including the biggest ones, like Valhalla, Loom, and Panama) need considerable changes to CPU-architecture and OS-specific code, all source code related to the Solaris OS and SPARC architecture is removed from the latest release. The ports were already deprecated in JDK 14, and their removal in this release was not unexpected. Such a measure will help the OpenJDK community concentrate efforts, move HotSpot further, and enable the use of C++14 language features for HotSpot in JDK 16 next year. Liberica JDK’s LTS versions 8 and 11 will continue supporting both Solaris and SPARC—till 2031 and 2027, respectively. The change only concerns JDK 15 and further releases. 6. Disable and Deprecate Biased Locking (JEP 374) Biased locking is a legacy technique used in the HotSpot Virtual Machine to reduce the overhead of an uncontended lock and optimize synchronization. JDK 15 is the first release where biased locking is off by default (still enabled in LTS releases, 11 and 8). It can only be turned on if -XX:+UseBiasedLocking is set on the command line when HotSpot is started. The reason for this JEP is that the technique happens to be precariously costly to maintain while its performance gains are less evident than they were seen before. JEP 374 follows, in a way, JEP 312 from JDK 10, where thread-local handshakes were proposed to optimize the Hotspot safepoint mechanism. Note that certain Java applications may experience a drop in performance. That is why the community has left the option of re-enabling biased locking via the command line—to mitigate regression and get insight into when it might be useful nonetheless. Liberica JDK 15 also contains a custom feature: more root certificates since JEP 319 in JDK 10. BellSoft has added a certificate bundle from Mozilla CA Store, as Mozilla is the most trustworthy source of NSS root certificates. You can now download the Liberica JDK 15 builds here. - [Java microservices: architecture, best practices, tutorials](https://bell-sw.com/announcements/2020/09/21/microservices-101-understanding-the-architecture/): There is hardly any developer left who hasn’t heard about the microservices. So much is written about microservice architecture, and yet you might still stumble through the basics. In this article, we give the definition of microservice architecture, discuss Java microservice frameworks, containers, native images, and the intricacies of creating and supporting microservices from a Java™ language perspective. You will find out how to turn your application into a working microservice. Intrigued? Dive right in! Table of Contents What are microservices? Java Microservice Architecture Contexts and Dependency Injection Dynamic proxies Web Server Java Virtual Machine Containerization Benefits of microservices Java microservices frameworks Spring Boot Jersey Micronaut Microservices best practices Additional tutorials What are microservices? The microservice architecture is a lightweight method of organizing software as opposed to monoliths. Previously, applications were mostly developed on a single codebase as centralized entities. Such an application had dozens of functionalities that processed tons of data. This in turn led to updating or troubleshooting being a difficult process: one tiny requirement made the team change the entire monolith. However, this style of software development was great for its purposes until applications became more prevalent on mobile devices and the cloud. When your back-end data is on different and numerous devices, monolithic architecture won’t cut it. This is where service-oriented architecture (SOA) comes in. SOA was an attempt to solve the downtime issue by dividing the structure into discrete units of similar functionality, i.e., services. Microservices are a variant of SOAs. Such services were designed to avoid the risk of software bloat. Microservices are smaller in size and communicate over a network through language-agnostic protocols, like APIs. It gives developers more freedom in choosing production tools since they don’t have to rely on enterprise service buses, other services, and the way they couple. Then, thanks to advances in containerization, parts of an application with microservices have become even more autonomous. Now business components of your application can be controlled individually while running simultaneously on the same hardware. Microservices in containers give way to cloud-native computing along with efficient and scalable applications. And if you use tiny containers built upon the inherently lightweight Alpine Linux, you will be able to reduce deployment time and cloud expenses significantly. Java Microservice Architecture If your team already has a monolithic application that grew out of its shoes, you can split it into a system of microservices. With time you can add new services that take off some load off the existing ones. Below you will find the tools that will help you to create microservice architecture in Java™. Contexts and Dependency Injection In Java EE, Contexts and Dependency Injection is a feature that helps to control the lifecycle of stateful objects using domain-specific contexts and integrate various components into client objects in a type-safe way. Annotations or various means of external configuration make it possible to animate plain class fields. Containers or frameworks that have control over class/bean lifecycle can inject necessary fields after constructing them in a sophisticated manner. Original code will not be affected by that complexity and will not be disrupted by a replacement for testing or changing the way of communication. The underlying principle of such architecture is called Inversion of Control (IoC). Dynamic proxies When we create service architecture, the role of reflection is critical. It powers discovery mechanisms, and components/beans find each other in run time, taking only required actions. In Java™, we can create a facade instance that intercepts interface method calls. This design pattern is called a Dynamic Proxy and is made with the help of java.lang.reflect.Proxy and java.lang.reflect.InvocationHandler. This is a built-in reflection API mechanism, widely used in CDI containers, e.g., in Spring. Web Server Frameworks allow the developers to plug in tunable components on a high level. But don’t forget about the low level programming and a whole world of lower-level communication protocols like HTTP where we need an intermediary between network connections and business logic. There’s also a need to manage some state (contexts), resources, and security. That’s what web servers do. Each microservice is coupled with its server instance configured in a centralized manner with the framework’s help, like Embedded Tomcat. Java Virtual Machine JVM and the core class library serve as a foundation for the web server, libraries, and business logic. The developer’s bytecode is verified and executed by the runtime and may be easily examined by standard diagnostic tools like Mission Control. JVM tuning is involved in the tuning of the web server, framework, libraries, and the application as well. As they typically allow customization, it’s natural to configure them all at once. In addition, JVM may be updated independently of business logic. So security updates, new features, and optimized defaults go into production with the main code intact. Containerization Containerization is a modern technology that makes all OS and external communications abstract without the downsides of hardware virtualization. Container management systems like Docker or Podman let us define, publish, and run container images with various software. Operating systems provide necessary levels of isolation and constrain resources for each container instance as requested by a management tool. Software components in a container image form a stack. The software stack makes physical deployment of a microservice more effective. When top levels are updated, we may reuse cached lower ones. Thus, binaries from cached layers aren’t transferred, and other preliminary actions do not perform. A layered image looks like this: The job of developers is to configure all these parts in such a way that they interact with one another, work correctly in a host OS, and communicate with external systems. Finally, there are orchestration systems that make it possible to distribute containers over a cluster of machines, like Kubernetes and Marathon. Here, traces of Java™ code and JVM disappear, building blocks become the configured containers deployed over the server fleet, and they may coexist with SaaS components available in a cloud. Benefits of microservices There are multiple instances where developers might find microservices quite useful. Scalability. Microservices are a perfect technology if you plan to scale your application up or down (vertically with CPUs/memory), out or in (horizontally with nodes/desktops/servers). Such flexibility is granted by each service being independent. Plus, it is possible to alter the application dynamically: turning components on and off to balance computational loads. Cloud-native apps. Microservices go well together with Docker containers, where they are encapsulated along with runtimes, frameworks and libraries, even an operating system. This way, every business function is separated, maintained, and deployed in the cloud as one unit. Undemanding maintenance. Smaller services are easier to test and debug. The microservice architecture is very straightforward: here’s an interface, its implementation, and a defined number of services. You can replace some of them and don’t touch the rest. Combined with continuous delivery, this design model drives you closer to delivering fail-proof products to your customers. High resiliency. Since an application is decoupled into independent services, it’s more resistant to errors. A bug in one part of the code will not cause the entire system to shut down. Instead, the team will only have to fix a service. Overall economy. A benefit in three parts: Building an application as individual services means short release cycles. An enterprise doesn’t need a complete redesign to modernize one service; it uses continuous delivery with cross-functional teams and deploys services independently. Containerized images with microservices use file systems such as UnionFS, AuFS, OverlayFS. They create layers that describe how to recreate actions done to a base image. As a result, only the number of layer updates increases, leaving containers lightweight, saving valuable resources and reducing company expenses. When functionalities are autonomous, you can assign separate teams to work on each. They don’t even have to be at the same place. In addition, your engineers will be able to deploy microservices with the simplest devices (read “x86 32 bit”) so you don’t need to equip yourself with pricey machines. A wide selection of tools. The microservice architecture helps to avoid vendor lock-in. If components don’t depend on one another, they may run on different frameworks or use a number of libraries. Developers might even choose several languages to write code for a single application. Independence from the database. When a service gets separated from the system, the database links get very flexible because of a unified interface. It is possible to make changes to the data or even replace the database entirely. Java microservices frameworks Frameworks are critical to implementing many routine tasks within microservices. They are very abundant in our industry, but they don’t exist in a vacuum and sometimes share common APIs. It is not uncommon for frameworks to support multiple APIs because they allow porting existing code as is or with minor changes. Below you will find the most useful frameworks for building microservices in Java, as well as sample code to launch your demo apps. Spring Boot Spring Boot, a part of an extensive Spring ecosystem, is the most popular framework in Java development. Spring Boot uses an embedded server Tomcat thus eliminating the need for Java EE containers. It also supports IoC, AOP, and has various projects for convenient microservices development, including Spring Data for microservices associated with data access; Spring Security for integrating authentication and authorization features; Spring Cloud for building distributed systems. In addition, the Spring ecosystem was recently enriched with Spring Native, a platform for native image construction. We will talk about the benefits of native images later, but for now, let’s create a sample microservice in Spring Boot. Starting with Spring Boot couldn’t be more convenient. Use a spring initializr to create a web server. Select Maven Project, the latest Spring Boot version and the Java version you are using. Add Spring Web dependency. Finally, download the project, open it in your favorite IDE, and add the following sample code to the DemoApplication class: import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @RequestMapping("/") public String hello() { return "Hi there!"; } } That’s it! You can launch the app, and it will listen on http://localhost:8080. Jersey Eclipse Jersey is a Java framework aiding in RESTful Web Services development. Jersey implements the JAX-RS API and makes the creation of RESTful services a lot easier. To get started with Jersey, study the documentation at the developer’s website. You can create a Maven project, run mvn archetype:generate -DarchetypeArtifactId=jersey-quickstart-grizzly2 \ -DarchetypeGroupId=org.glassfish.jersey.archetypes -DinteractiveMode=false \ -DgroupId=com.example -DartifactId=simple-service -Dpackage=com.example \ -DarchetypeVersion=2.35 This will generate a Jersey project on top of a Grizzly container, which you can mend as you see fit. Micronaut Micronaut is a freshman in the world of Java frameworks, but it was created especially for facilitating microservices development. It includes built-in cloud-native support and AOT compilation, and is compatible with several programming languages: Java, Kotlin, and Groovy. To start with Micronaut, create a demo application at micronaut.io/launch. Then, create a HelloController class and add import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Produces; @Controller("/") public class HelloController { @Get @Produces(MediaType.TEXT_PLAIN) public String index() { return "Hello World!"; } } The application will answer with “Hello World!” at http://localhost:8080. Microservices best practices Microservice system can quickly spin out of control due to its complexity and turn into an intricate web that even senior developers will find hard to unweave. How to solve this problem? Decouple services with event streaming platforms (Kafka, Kinesis, Spark Streaming), simplify the service-to-service communication with a service mesh (Istio, OSM on Kubernetes), or redesign the system. Optimally, your goal is to avoid this: …by turning it into this: Note that the first scheme doesn’t feature any connection between message queues (represented by “MQ” cylinders) and databases. Also, if we look at the upper level, right below the user, we can see that it retains substantial complexity. Don’t forget that it’s rare to have only one application copy within a service. When they are numerous, it is necessary to understand where to address requests in case of failure. A service mesh network is meant to facilitate request delivery. Inside it, requests do not leave their individual infrastructure level, routed between microservices via proxies. Individual proxies here are called “sidecars” because they go alongside each service or application. The sidecar proxy design pattern is also used outside of microservices. It eases the tracking and maintenance of applications by abstracting particular features away from the central architecture: interservice communication, security, and monitoring. However, even when we have our microservices running, configured, and distributed, there is another issue that calls for our attention. How to minimize memory and time consumption? And here’s where we turn to the Native Image technology for a solution. First, how does Native Image work? Developers change a service binary form to a native executable with the help of GraalVM Native Image technology. A mix of Graal compiler applied in AOT mode and SubstrateVM virtual machine promises instant startup time, low memory consumption, short disk space requirements, and excellent performance. The native image runs Java™ microservices in closed-world assumptions. Base container level must provide minimal functionality (and there’s no JDK), which means it may be a “scratch” base image. It will work if a deployed application brings its dependencies: e.g., a native image may be statically linked with a standard C library. To facilitate the creation of native images, BellSoft developed a special tool, Liberica Native Image Kit. It allows creating microservices in different programming languages and supports a wide range of JDK versions and platforms so you can create an extensive network of microservices without worrying about compatibility. Discover Liberica NIK Running your Java™ project in a native image is obviously a more attractive choice in performance and speed. However, picking either runtime or native image for building the microservice architecture depends on the tasks at hand, business environment, the state of your enterprise, and many other aspects. Additional tutorials If you’d like to put theory into practice, you can build your own application using our series of Java microservices tutorials: Building a Java microservices application Deploying Java microservices Getting Java microservices ready for enterprise use We also prepared a set of tutorials on how to integrate Liberica Native Image Kit with popular Java frameworks: Creating microservices with Micronaut and Native Image Kit Integrating Quarkus with Native Image Kit Spring Native: a powerful addition to Spring ecosystem Microservices have the status of a young but promising technology. If you feel that your company is ready to disrupt the status quo and break up its monolithic golem, give microservices a try. And if you need expert help our Java professionals are ready to consult you on the matter. Contact our experts - [Liberica JDK release accessibility convenience in developer and user scenarios](https://bell-sw.com/announcements/2020/09/30/Liberica-JDK-release-accessibility/): We at BellSoft emphasize that Liberica JDK must be convenient and accessible to all users. Our development team makes sure the runtime is available as Docker container images, on package managers, clouds, and other distribution channels. Working on various operating systems and cloud storage platforms suffers from JDK’s lengthy deployment and updating. We have designed a specialized API to deploy releases and update quickly and uniformly in different scenarios. Say, a server application v3.3.1 on Windows Server should always have JDK 8u242 installed while the client on any other operating system is running on the latest version. Another example is a slack bot informing of a recent Liberica update suitable for the project. Or configurators to choose JDKs or JREs — possibilities are endless here. Here in this article, we will provide a unified approach to obtaining the newest Liberica JDK versions and working with them in even the most primitive environments on virtually any operating system. The choice dilemma We thought of making some steps towards the ease of distribution after analyzing user behavior on the product website and finding out that visitors parsed it for download links from scripts or their programs. The decision was then to create a public API for everyone’s convenience. Determining the layout and form of the API took considerable time, even longer than actually designing it. Liberica JDK is a ‘broad’ product with several major versions, multiple supported architectures, installation options, and binary assemblies, all supported in parallel. Releases come out more often than four times a year, and the number of artifacts in each can be hundreds. 2020 has already seen five new releases. In January: versions 8u242, 11.0.6, and 13.0.2; in March: 14.0; in April: 8u252, 11.0.7, and 14.0.1; in July: 8u262, 8u265, 11.0.8, and 14.0.2; and in September: 15.0. A total of over 1,000 artifacts have been released. It is quite difficult to parse and analyze such a large amount of data fully; and if you selected a wrong binary, the consequences could be quite grave, from spending extra memory on functionality unused in the application to security problems. Package managers do not solve the issue, as they simply install specific versions or builds and do not help make the right choice. A good API should be able to answer the following user questions: I know what I want, would you please give me a download link and checksum? I have version XYZ installed, does it contain the latest fixes? Applications for the API The main application for the API in light of the different updating mechanisms is its versatility and the convenience it grants users in streamlining their work with Java. The API provides structured, filtered, up-to-date build information over networks in a unified manner. The API also assists in parsing HTML pages, a laborious process that takes up valuable development time. At its core, an API is a reliable contract that users can employ to build a stable software script with minimum time constraints and less hassle in searching for the needed JDK versions. There are three ‘ideal’ use cases for the API: The team needs notifications only (a ‘get updates’ case). This case is for, more often, companies using modern CI/CD pipelines and containers. So when a new Liberica JDK version appears on their radar, it first goes to developers as a base container image. It spreads to test environments, then deploys on a few machines and, finally, covers the whole production. A helpful addition to the API here is writing a bot that will instantly ping you when a new version is released. The team prefers ruled updates (a ‘get updates’ case). Say, at some point, a company decides to upgrade servers or desktops. If it uses a Linux package manager, the standard system update is triggered—that is rather simple. If not, or if the team works on Windows, there may be an agent that can perform or skip an update by a centralized command or when some conditions become true. In such an instance, it is crucial to verify the checksum also provided by the API to ensure that the downloaded artifact is exactly what BellSoft has built and tested. When a particular version is installed, the API clears up which download links are used for every operating system. The team uses or develops a package manager (a ‘get versions’ case). A private artifactory system may perform extra checks and provide DMZ access. But again, it is essential first to compare checksum pulled in binaries. There may be no actual file storage (but a proxy), and original links could be provided by an internal company API online or offline. A good example of the last one is how JetBrains use the API. Recently, IntelliJ IDEA has offered a possibility to install JDKs produced by numerous vendors, BellSoft included. Project settings -> SDKs -> + -> Download JDK… As illustrated, this dialog’s content comes from BellSoft’s API: IntelliJ IDEA is looking for the latest Liberica JDK (14.0.2 at the time of writing) and the latest LTS versions for JDK 8 and 11. We offer the API to guarantee that its URL and protocol contain the latest information. More importantly, the approach is well documented and complies fully with industry standards. Our engineers are always looking for ways to make the products better and welcome feedback from users. Such an approach becomes ever more productive in the API case for allowing them to update the API and improve user experience. Release The most important implementation aspect is the download link to acquire or update the JDK. For convenience, our team has selected the file name to be a separate native ID, thus allowing the user to check that the artifact was downloaded correctly. The next step is the Liberica JDK version. A version in the Java world is a rather capacious concept with an entire JEP 322 specification dedicated to it. But it only works from Java 10 onwards. For Java 9, you would refer to JEP 223. API use convenience This diversity leads to two main issues. The first is that the components of the versions in these specifications are named differently, leading to constant updates of Java 8, which has no such specifications. Besides, the notation in JEP 322 is less intuitive than in JEP 223: e.g., ‘feature’ instead of ‘major’, ‘interim’ instead of ‘minor’, etc. Based on the given facts, our development team always decided to use the new notation and decompose the Java 8 and 9 versions into new fields. In total, six more fields are added to the original object: "Version": "11.0.7+10", "featureVersion": 8, "interimVersion": 0, "updateVersion": 7, "patchVersion": 0, "buildVersion": 10 Next, numerous fields for describing the current artifact: operating system (os); architecture; bitness; installation type — archive or package for automatic deployment (installationType); package type — file extension in its essence (packageType); bundle type (bundleType). If the first five properties are obvious, the sixth requires some explanation. One version of Liberica JDK is released in three separate sub-versions (six in fact, because of JDK and JRE): Standard — regular JDK or JRE; Full — Standard, with JavaFX added; Lite — Standard, with specific features optimized by jlink. To ensure that all Liberica JDK’s users know which versions to choose, we introduced several assisting functions, namely: The LTS (Long term support) field — set to true for versions 8 and 11. The EOL (End of life) field — for Liberica JDK versions no longer updated (9, 10, 12, 13). The latestInFeatureVersion flag — indicates whether the artifact is the latest among feature versions. The latest flag — indicates whether the artifact is, in fact, the latest among all the existing versions. Below you will encounter some more abbreviations, which mean the following: GA — General Availability. Stable release. EA — Early Access. Unstable release, not shown in the examples here. FX — JavaFX is present. Editions glossary: jre — JRE, runtime. jdk — JDK is also a classic. JRE + development features. jdk-full — JDK + JavaFX; in fact, it also includes the Minimal VM for embedded solutions on platforms where relevant. jdk-lite — TODO. jre-full — JRE + JavaFX. Framework The Spring framework from Pivotal (BellSoft’s partner), running on Liberica JDK, was chosen for the API backend implementation. The server receives data for operation from the BellSoft GitHub repository. During development, it was decided to add separate endpoints. The API supports filters and returning text responses if users do not want to parse JSON strings. You can find detailed technical documentation on the Open API and try the API in action here. Several examples of its use: Find a link to the required artifact. Or get the link as plain text. 2.1. Check if a certain Liberica JDK version is the latest one available. The response should contain “latest”: true 2.2. Check if a certain Liberica JDK version is the most recent LTS version. The response should contain “latestLTS”: true Our developers plan to add all the company’s products, like Liberica Mission Control and others, to this API. User end Earlier, the article presented the three common use cases for the API. From this point forward, it features a case study of BellSoft’s API from a Liberica JDK user Vladimir Bogdanov, who works as a system administrator at Voronezh State Technical University. The team has specific requirements (a ‘get versions’ case). It frequently provisions new machines with a toolset using helpers like Ansible. API is very flexible here, e.g., it selects the latest LTS versions, and configuration is controlled in platform-independent and ‘write once’ style. The team also works in a physical hardware lab for educational and scientific purposes. It has about a dozen x86_64 machines running Windows. Frequently, a machine might break, or the OS might stop responding, or a new host might require setup. There is a defined set of configuration steps with software like IDEs and research tools installed, including JDK. BellSoft thanks Vladimir for his insights and an unusual application for the API. I first felt the need for adequate software and proper installation guides a decade ago, in 2010, when the school I work at brought in computers. Over time, the number of machines increased, as did the requirements for educational tools. On top of some run of the mill Autocad and Word, informatics teachers asked for 7th Delphi, VisualStudio, MinGW, as well as NetBeans and Eclipse (for the ‘Java-related purposes’). We managed this menagerie of applications under our own steam, as there was no alternative. Many different builds and incompatibility issues led to disarray and software’s improper functioning. Deployment was a mess; development times were staggering. These times bred vast numbers of add-ons and versions of the same programs. When after years of inconvenience, I stumbled upon BellSoft’s API, I was excited. It allowed working with the many JDK versions via a single access point. Identifying the latest versions of compatible software Given the added deb-based and rpm-based distributions, the main question ensuing from the short introduction is: How could I find out the latest stable LTS version for 64-bit Windows with an MSI installer on an ordinary (Intel or AMD) computer, containing JDK, JRE and JavaFX? The following is one of the methods: curl https://api.bell-sw.com/v1/liberica/releases?version-modifier=latest&bitness=64&release-type=lts&package-type=msi&bundle-type=jdk-full At the time of writing, this request returned “11.0.8”. The same is applicable for Windows PowerShell. Type in Invoke-RestMethod and insert the same link as above in the command line. { "bitness": 64, "buildVersion": 10, "EOL": false, "latestLTS": true, "os": "windows", "updateVersion": 8, "downloadUrl": "https://github.com/bell-sw/Liberica/releases/download/11.0.8+10/bellsoft-jdk11.0.8+10-windows-amd64-full.msi", "interimVersion": 0, "latestInFeatureVersion": true, "LTS": true, "bundleType": "jdk-full", "version": "11.0.8 + 10", "featureVersion": 11, "packageType": "msi", "sha1": "", "FX": true, "filename": "bellsoft-jdk11.0.8+10-windows-amd64-full.msi", "installationType": "installer", "size": 266248192, "patchVersion": 0, "GA": true, "architecture": "x86", "latest": false } curl --silent https://api.bell-sw.com/v1/liberica/releases downloads JSON without unnecessary details about speed, time and file size, and then uploads what was downloaded onto the console. Windows 10 has it out of the box on its official website. On Linux, you need to type in sudo apt \ yum \ dnf \ zypper \ apk -y and install curl depending on the distribution. "os": "windows" and "bitness": 64 are clear from looking at the one-liner and comparing it with the example above. To get a download link, perform the same operation but change featureVersion to downloadUrl: curl --silent "https://api.bell-sw.com/v1/liberica/releases?package-type=msi&version-modifier=latest&release-type=lts&bundle-type=jdk-full&bitness=64&fields=downloadUrl&output=text" Note that "featureVersion": 11 is the same as sudo apt-get install bellsoft-java11 from the Linux installation example. Thus, the string to install the latest LTS version on Ubuntu will look like this, after adding the repository, key and updating the indexes: LATEST_LTS_LIBERICA=$(curl "https://api.bell-sw.com/v1/liberica/releases?package-type=deb&version-modifier=latest&release-type=lts&bundle-type=jdk-full&bitness=64&arch=x86&fields=featureVersion&output=text") sudo apt -y install bellsoft-java"$LATEST_LTS_LIBERICA_JAVA" Ansible approach to installing JDK I have also published an Ansible role to install the necessary versions of JDK/JRE. As mentioned before, our lab has specific requirements for the API—a broad spectrum toolset, IDEs, and unstable hardware—so it is quite natural to use Ansible for this task. Repeat the same but adopting the syntax of your configuration management system (to obtain the release name): Variables: liberica_common_architecture: x86 liberica_common_bundletype: jdk-full liberica_common_eol: 'false' liberica_common_lts: 'true' liberica_common_ga: 'true' liberica_common_latestlts: 'true' Extract the link with information and write it to the liberica_releases variable. - name: Get releases info from API block: - uri: url: https://api.bell-sw.com/v1/liberica/releases method: GET return_content: yes delegate_to: localhost register: liberica_releases set_fact: liberica_version_number_fact: "{{liberica_java_version | default (liberica_releases.json | json_query (query_feature_version))}}" vars: query_feature_version: "[? architecture ==` {{liberica_common_architecture}} `] | [? bundleType == `{{liberica_common_bundletype}}`] | [? EOL == `{{liberica_common_eol}}`] | [? LTS == `{{liberica_common_lts}}`] | [? GA == `{{liberica_common_ga}}`] | [? latestLTS == `{{liberica_common_latestlts}}`] .featureVersion | [0] " Some more explanations: liberica_version_number_fact is the name of the variable that will contain the “11” mentioned before. liberica_java_version is the version that, in theory, the user should set if they need a particular one. If nothing is specified, the default, or everything inside default(), is executed. liberica_releases.json is the proprietary register: liberica_releases variable. Ansible understands if it is given JSON as a reference and puts it in a separate json key. json_query is a standard Ansible function. It is an overlay for jmespath. Note that it is not a regular BellSoft API query, but a query string for the jq tool used in this role as an alternative way of extracting information. Everything below query_feature_version: is the same as the preceding code, only split into several lines, because ansible-lint was not compatible. The double parentheses are part of jinja2’s templating and contain the variable name. Same as $VAR in bash. I should also mention the quotes. This bracing is specific only to jmespath. Conclusion User convenience is at the core of the API release from BellSoft, as multiple distributions are connected with issues related to their updating. Our development team aims to push its product this year and works hard to implement new even more user-friendly features. Liberica Download API is a universal solution that works for all the cases described in even the most primitive operating environment. It is available in IntelliJ IDEA or on our website. The many problems faced on different systems can be negated by installing a safe and unified Liberica software package to work with the JDK. - [How BellSoft ensures Liberica JDK quality](https://bell-sw.com/announcements/2020/10/19/How-Bellsoft-Ensures-Liberica-JDK-Quality/): We at BellSoft take nothing more seriously than the quality, security, and performance of our products. These are the most common concerns for any organization that chooses to develop with OpenJDK. No need to worry—you do not have to “only pick two.” In this material, we go over the main components that constitute Liberica JDK quality control (or QC for short). The goal is to explain how we work on the Quality–Security–Performance triangle, manage to attain a balance, and produce the best Java runtime we can. Check it out for yourself: click the button below to see all its benefits and Liberica binaries distributed entirely for free. DISCOVER LIBERICA JDK Release cycle Before we unravel our QC intricacies, let’s look into why we need it, exactly. OpenJDK Critical Patch Updates (CPUs), containing fixes to vulnerabilities, are date-driven and released quarterly. These are not insignificant; for instance, last year saw 1957 defects and 95 CVE fixed or backported in JDK 8 and 11 combined. In addition to that, major feature releases are developed in a 6-month cadence. Plus, all vendors customarily deliver bug fixes to customers before or after the next feature release. Now that we have described the existing state of affairs for OpenJDK in general, what about BellSoft specifically? What does our patch flow look like? As you can see, the process is somewhat confusing and has many actors. To reach eventual consistency with OpenJDK, to ensure all Liberica JDK releases are up to par, QC is simply imperative. Quality criteria for releases In 2020, the Java™ programming language celebrated its 25th anniversary and is still thriving. BellSoft can acclaim to meet the high standards to quality the industry has set over this time. Let’s review the main criteria for a compliant release. A Technology Compatibility Kit (TCK) is a set of tests provided by Oracle. They indicate if a particular Java™ implementation is compatible with Java SE specifications. This suite is the Swiss army knife of QC. It indicates that a Java™ platform is highly performant and will not damage your critical applications. All Liberica binaries have passed the TCK tests and are fully ratified in the Java Community Process. As for the numbers of tests in JDKs (Java Compatibility Kits) for the latest LTS releases and current 15, here they are: JCK-8c: over 126,000 JCK-11: over 139,000 JCK-15: over 152,000 One of the commonly used approaches to security analysis is proof-of-concept (PoC) exploits. They are non-harmful attacks meant to unveil software weaknesses. In our case, the engineering team develops a piece of PoC code to verify whether a particular OpenJDK vulnerability is exploitable. Once a security flaw is found, we provide a fix. This process is repeated for every release. Another part of quality control at BellSoft is functional OpenJDK tests executed by the Java Regression Test Harness (jtreg). It is used for functional and regression Java SE testing, which belongs strictly to a TCK. We run somewhere between 11,000 and 34,000 regression tests per binary before pushing a new Liberica JDK version, including hotspot, jdk, langtools, nashorn and, in certain cases, jcstress. A set of OpenJDK regression tests is also complemented by the largest industry frameworks: Spring and Apache Tomcat. During the most recent check, BellSoft performed more than 21,000 tests that are part of Spring and 40,000 from Tomcat (including APR connector tests running with Tomcat Native), which revealed no regressions. Benchmark testing is as essential as all the previous steps. There are two sides to this process. First, we evaluate Liberica JDK’s core functionality with major industry-standard benchmarks, such as SPECjbb and SPECjvm. Second, our team runs microbenchmarks embedded in the OpenJDK code. Based on the community-developed Java Microbenchmark Harness (JMH), they measure the performance of individual functions. How do we make it happen on time? Work on each release starts well in advance, sometimes more than a year before. It depends on the scope of work and helps us not rush the development. Key factors for timely quality control: push all test patches upstream, to the mainline JDK branch, as soon as they are developed; agree within the OpenJDK Vulnerability Group on the scope of each release. Since it is combined of engineers from major distributors such as Oracle, SAP, Amazon, and Red Hat, BellSoft has a clear understanding of timelines; test all platforms with and without security fixes; verify that final testing right before the release finds no new issues in the build but reconfirms its finished status; make sure binaries stored on CDN are not corrupt during network transfer (using checksums). The role of CI/CD pipelines An OpenJDK distributor does quite a lot of testing to a single build. And yet, this lot pales before the massive number of binaries to be checked before release. 12 platforms (Windows x86 32/64 bit, macOS, Linux x86 32/64 bit, Alpine Linux 64 bit/musl, Linux ARM 32/64 bit, Solaris Sparc/x86) 3 JVMs (server, client, minimal) JDK and JRE 7 types of installers: msi, pkg, dmg, deb, rpm, zip, tar.gz 3 presets: lite, standard, full Multiplied and with thousands of Java and JVM command-line options added, this impressive array is impossible to test manually. Strict guidelines that help with timing are great. But no amount of discipline beats automation. Continuous integration and continuous delivery (CI/CD) systems enter the scene. A way to implement a CI/CD system, or pipeline, is to use Jenkins as a core technology. It allows plugging in APIs, software libraries, and build tools. On its own, Jenkins does not have any functionality but gets powerful with other various tools. Groups of events or jobs are aligned together in a sequence. Automated Liberica JDK building and testing is Jenkins-based and is executed on nodes, computers with various OS and microprocessor architectures. Not all nodes are up simultaneously; they are only enabled when needed. BellSoft has a minimum amount of them on at all times for production, to fix own and customers’ bugs. At peak load during the release, we engage extra nodes, and the whole CI farm with cloud instances accounts for 400 to 1,200 nodes. CI/CD pipelines are one of the reasons we manage to finish Liberica JDK quality control on time. Final stages Above, we have discussed events happening several weeks before the release. Everything we do is to be sure that day X goes without a hitch. On the penultimate day, X-1, we prepare Liberica JDK bundles, make release notes, and push the builds to content delivery networks (CDNs) for a better user experience. Extra patches for customers Finally, open source JDKs would not evolve so rapidly if it was not for the community. BellSoft is among the top 5 OpenJDK contributors because of the number of fixes and proposals the company makes. However, we have ranked this high thanks to the work we do in cahoots with Liberica JDK users. By providing early patches and binaries for testing to our customers, we get to find solutions to known (and sometimes hidden) issues much faster. Furthermore, we send patches to functional issues before the next update release to expedite their resolution. Want to get valuable patches before the rest of the world knows about them? Contact our engineering team. We will examine your unique case to offer what is best for your enterprise. Learn how to make the most out of the high-quality Java runtime that is Liberica JDK. GET FREE ADVICE - [JVM in Linux containers, surviving the isolation](https://bell-sw.com/announcements/2020/10/28/JVM-in-Linux-containers-surviving-the-isolation/): Running a Java application in Docker on a VM hosted in the cloud is not uncommon these days. But let’s take a closer look at this setup. We have a bare metal box somewhere in the cloud provider’s data center and hypervisor host OS running on that box. Next, we have a guest OS running in a VM provided by the hypervisor. Docker is running in the guest OS and provides a container runtime. Not to mention JVM in the container, which is also a type of VM. To sum it up, we have virtualization, container, and JVM all inside each other, stacked as a nesting doll. A big promise that container technologies give us developers is being able to control the environment for applications to run. Ideally, once packed in a Docker or Podman container, the application should behave the same regardless of where it started. Sometimes, things do not work as expected, and you have to learn the internals of Linux container technologies to fix problems. In this post, I would like to cover a few typical caveats of container resource management and networking with a view on JVM. Table of Contents Virtualization How are containers different from virtual machines? Why containers instead of VM? Why do I need to care about containers? Honoring the limits Russian roulette with oom killer Native memory tracking Networking VPN and the mystery of hanging connections Kernel level resource limits JMX in container Conclusion Related Posts Virtualization While this post is specifically about JVM and Linux containers, I would like to explain how containers are different from virtualization first. The key idea of virtualization is to execute normal binary code inside a VM retaining semantic of CPU architecture but controlling resource usage and device access. A security boundary around the VM is an essential part of virtualization. Today virtualization typically utilizes CPU support, which helps to create a low overhead sandbox for guest OS. This also means that host and guest OS’s should have the same CPU architecture. Virtualization is also possible without special hardware support by simulating the execution of CPU instructions in software. Simulation-based virtualization, while slower, is still practical for running guests having CPU architecture different from the host (e.g., running ARM-based Android OS on x86_64 host). JVM is also a simulator of sorts; it is running JVM bytecode on your host CPU architecture. And surprise, surprise, both JVM and modern CPU simulators are using JIT (Just in Time) compilation to improve simulation performance. JVM is not a real hypervisor, even if there are many parallels between JVM and virtualization hypervisor. How are containers different from virtual machines? Many people mistake containers for a kind of virtualization technology and form certain assumptions based on that misunderstanding. The major difference is that each VM on the hypervisor has a separate OS kernel (including own memory management, CPU scheduling, etc.). In contrast, all containers on the same host share the kernel between each other and the host. “Yet, I can run a RHEL container on my Ubuntu host, can’t I?” Yes, you can, although your RHEL would run with a kernel from the Ubuntu host (typically a few versions ahead). Thanks to Linux kernel stable contracts for syscalls, things just work even if userspace binaries were built for an older Linux kernel version (most of the time, at least). “Hey, but I can run Docker on my macOS. Where is the Linux kernel there?” It is there alright, just running using virtualization, and all your Docker containers are running in Linux VM silently sitting on your macOS. Containers are not a virtualization technology is an important fact to keep in mind. But what is a Linux container then? My casual answer is: Linux containers are just a glorified chroot. Virtualization creates VM boundaries by controlling the execution of CPU instructions; the container boundary in Linux is created by controlling kernel syscalls. Kernel syscalls surface is much wider, with multiple distinct Linux features rigged together to enforce container boundaries. Below are key features used for Linux containers. cgroups is an important feature used to control resource utilization for containers. CPU and memory limits in containers are enforced via cgroups. Dedicated FS mounted as root in the container. For Docker, image content is mounted as root, other containers (e.g., LXC) may use a full-fledged file system on block devices for this purpose. Namespaces are another feature critical for container isolation. This feature of the Linux kernel makes it possible to control kernel resource visibility at the process level. In particular, it is used to control individual FS mounts, network devices, and user accounts at the container level. Virtual ethernet adapters and NAT are used to create container network isolation. Typically, a container would have its own set of network adapters that are connected to the outside world via NAT. Container runtime needs to do a lot of plumbing and may make features above work together consistently to create an experience of container isolation. Why containers instead of VM? The main reason is efficiency. The process started in the container is just another process started at the host OS. There is virtually no overhead between containerized and non containerized process execution. Virtualization, even hardware-assisted, always brings a noticeable toll on resource utilization and startup time. Although virtualization is evolving and new optimizations are popping up, it would never become a zero-cost abstraction. Specifically, containers are very lightweight in terms of memory, while virtualization always urges you to have overhead for the copy of the kernel in each VM. Why do I need to care about containers? So we have a VM, inside a VM, inside a VM. “Why would I care?” you might ask. In an ideal world, you wouldn’t need to. Things just work there. However, ours is not an ideal world. JVM (Java Virtual Machine) is a VM. Despite it not being a VM as per virtualization terms above, it is still an abstraction layer between code inside JVM and outside OS. To build an abstraction efficiently, JVM needs to be highly aware of the OS it is running on. There is a lot of OS-specific code in the OpenJDK codebase, a requirement for Java code to execute properly. Containers are subtly challenging the status quo of how the OS behaves. And for JVM, a product with a long history, it is not always easy to adapt to the new rules of the game. Honoring the limits Memory and cpu limits are important features of containers. But they are controlled by cgroups and dangerously ignored by applications unless they have built-in support for cgroups. Try running docker run -m 512m ubuntu free on Linux console. Container memory is limited to 512 MiB by -m 512m option, though free command would report memory statistics for the host OS. Let’s now try a similar experiment with JVM and look at how it would restrict heap size depending on container size. \> docker run bellsoft/liberica-openjdk-debian:11 java -XX:+PrintFlagsFinal -version | grep -iE "InitialHeapSize|MaxHeapSize" size_t InitialHeapSize = 257949696 {product} {ergonomic} size_t MaxHeapSize = 4120903680 {product} {ergonomic} \> docker run -m 512m bellsoft/liberica-openjdk-debian:11 java -XX:+PrintFlagsFinal -version | grep -iE "InitialHeapSize|MaxHeapSize" size_t InitialHeapSize = 8388608 {product} {ergonomic} size_t MaxHeapSize = 134217728 {product} {ergonomic} It looks like JVM recognizes cgroups memory limit. With the growing popularity of solutions based on cgroups (Docker included), JVM has to rework its resource allocation heuristics. Special handling for cgroups was also added (see JDK-8146115). Changes were introduced in OpenJDK 10 and backported to Java 8. We can turn off this feature and see what happens. Ok, JVM can detect memory limit, but how value for max heap size is calculated anyway? MaxHeapSize now is calculated as -XX:MaxRAMPercentage * MEMORY_LIMIT where MEMORY_LIMIT is either cgroups limit or RAM available on the host machine. MaxRAMPercentage is 25% by default, which is probably too small for a container case. You can easily adjust it and still keep your JVM heap limit relative to the container memory limit. \> docker run -m 512m bellsoft/liberica-openjdk-debian:11 java -XX:MaxRAMPercentage=75 \ -XX:+PrintFlagsFinal -version | grep -iE "InitialHeapSize|MaxHeapSize" size_t InitialHeapSize = 8388608 {product} {ergonomic} size_t MaxHeapSize = 402653184 {product} {ergonomic} Besides memory limit, JVM acknowledges CPU limit and adjusts the number of GC threads based on container limits, too. -XX:+UseContainerSupport flag responsible for cgroups awareness is on by default in OpenJDK 11 and above and since OpenJDK 8u191. Russian roulette with oom killer Following the previous exercise, you may ask: “Why not set MaxRAMPercentage to 100%?” An important thing you should never forget regarding JVM memory is that heap size ≠ memory used. Without jumping straight into the rabbit hole of memory management, let’s play with container memory limits of container and JVM size and see what would happen. I have prepared a short code snippet to play with. import java.util.ArrayList; import java.util.List; public class HeapFiller { public static void main(String[] args) { Runtime runtime = Runtime.getRuntime(); System.out.println("Max heap size: " + (runtime.maxMemory() >> 20) + "M"); try { List data = new ArrayList<>(); long lastMemUse = runtime.totalMemory() - runtime.freeMemory(); while(true) { data.add(new byte[1 << 20]); long memUse = runtime.totalMemory() - runtime.freeMemory(); if (memUse > lastMemUse + (20 << 20)) { System.out.println("Heap usage: " + (memUse >> 20) + "M"); lastMemUse = memUse; } } } catch (OutOfMemoryError e) { System.out.println(e.toString()); runtime.halt(0); } } } Save code as HeapFiller.java and build the Docker image using an ordinary build file. FROM bellsoft/liberica-openjdk-debian:11 COPY *.java /app/ RUN cd /app && javac *.java Build the image. docker build -t testapp . Now let’s see what would happen with -XX:MaxRAMPercentage=100. /> docker run -m 512m testapp java -XX:MaxRAMPercentage=100 -cp /app HeapFiller Max heap size: 494M ... Heap usage: 465M Heap usage: 485M java.lang.OutOfMemoryError: Java heap space Looks fine, we have filled the whole heap and got OOME in our code. Let’s try to raise the heap size above the memory allowance of the container. /> docker run -m 512m testapp java -Xmx2g -cp /app HeapFiller Max heap size: 1979M ... Heap usage: 767M Heap usage: 787M Heap usage: 807M In this run, the JVM process is just killed. No “out of memory” or other errors, the container has simply disappeared. And still, if I run dmesg, the error is easily spotted. \> dmesg [97773.657826] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=80f0e7d30336357ce04b4dda36a72c79f0677913b9a5988357845e43cc7d5a54,mems_allowed=0,oom_memcg=/docker/80f0e7d30336357ce04b4dda36a72c79f0677913b9a5988357845e43cc7d5a54,task_memcg=/docker/80f0e7d30336357ce04b4dda36a72c79f0677913b9a5988357845e43cc7d5a54,task=java,pid=13413,uid=0 [97773.657872] Memory cgroup out of memory: Killed process 13413 (java) total-vm:4321312kB, anon-rss:518932kB, file-rss:19428kB, shmem-rss:0kB [97773.701215] oom_reaper: reaped process 13413 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB [97773.816474] docker0: port 1(veth8303ede) entered disabled state [97773.817047] veth658eeeb: renamed from eth0 [97773.894689] docker0: port 1(veth8303ede) entered disabled state [97773.898459] device veth8303ede left promiscuous mode [97773.898465] docker0: port 1(veth8303ede) entered disabled state This is the way memory limits are enforced in containers. Breach the limit, and you are dead. No warnings, no second chances. But why did we get killed at 807MiB of heap usage (and why log shows anon-rss:519MiB)? cgroups can control both resident memory and swap memory utilization. Docker option -m 512m is interpreted as 512MiB of resident memory + 512MiB of swap memory (though only if swap is enabled in the system). So the effective limit was 1GiB. Speaking of swap, it is typically enabled on desktops and small VMs. However, servers with high amounts of RAM often have swap disabled (e.g., most images on AWS except the smallest ones have swap disabled). Your container can run locally but crash being deployed on the target environment due to lack of swap. We can control the swap limit directly. Now it’s time to disallow swap usage. Combination -m 512m --memory-swap 512m will restrict total memory usage to 512MiB. \> docker run -m 512m --memory-swap 512m testapp java -Xmx2g -cp /app HeapFiller Max heap size: 1979M ... Heap usage: 445M Heap usage: 465M Heap usage: 485M Now the container is killed near its expected limit. Let’s try -XX:MaxRAMPercentage=100 again. \> docker run -m 512m --memory-swap 512m testapp java -XX:MaxRAMPercentage=100 -cp /app HeapFiller Max heap size: 494M ... Heap usage: 446M Heap usage: 466M Heap usage: 486M java.lang.OutOfMemoryError: Java heap space We got a JVM level error now. This application is very simple, though, just one thread and tiny code size. The example below both creates threads and fills the heap with objects import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Exchanger; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadSpawner { static CountDownLatch LATCH = new CountDownLatch(1); public static void main(String[] args) { Runtime runtime = Runtime.getRuntime(); int recursionDepth = Integer.getInteger("depth", 128); try { Executor pool = new ThreadPoolExecutor(Integer.MAX_VALUE, Integer.MAX_VALUE, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)); long lastThreadCount = Thread.activeCount(); while(true) { Exchanger ex = new Exchanger<>(); pool.execute(() -> recursiveTask(recursionDepth, "", ex)); ex.exchange(""); if (Thread.activeCount() > lastThreadCount + 50) { long memUse = runtime.totalMemory() - runtime.freeMemory(); System.out.print("Heap usage: " + (memUse >> 20) + "M"); System.out.println(" | Thread count: " + Thread.activeCount()); lastThreadCount = Thread.activeCount(); } } } catch (InterruptedException e) { System.out.println("Interrupted"); } catch (OutOfMemoryError e) { System.out.println(e.toString()); Runtime.getRuntime().halt(0); } } public static void recursiveTask(int depth, String input, Exchanger receiver) { try { if (depth == 0) { receiver.exchange(input); // block thread LATCH.await(); } else { recursiveTask(depth - 1, input + String.valueOf(new byte[10 << 10]), receiver); // prevent GC from collecting variable before the call input.length(); } } catch (InterruptedException e) { System.out.println("Interrupted"); } catch (OutOfMemoryError e) { System.out.println(e.toString()); Runtime.getRuntime().halt(0); } } } Save code as ThreadSpawner.java and rebuild the Docker image. Now let’s try to run it with 512MiB of memory limit. \> docker run -m 512m --memory-swap 512m testapp java -XX:MaxRAMPercentage=100 -cp /app ThreadSpawner ... Heap usage: 43M | Thread count: 409 Heap usage: 60M | Thread count: 460 Heap usage: 59M | Thread count: 511 With lots of threads, the container is killed much earlier reaching just 62MiB of heap utilization. It should surprise you at this point. Each thread requires a stack occupied outside of the heap. Can we reduce stack size? The default stack size is 1024 KiB. If we cut it in half, the container should survive more threads. \> docker run -m 512m --memory-swap 512m testapp java -XX:MaxRAMPercentage=100 -XX:ThreadStackSize=512 -cp /app ThreadSpawner ... Heap usage: 45M | Thread count: 409 Heap usage: 44M | Thread count: 460 Heap usage: 62M | Thread count: 511 No effect. The container has survived the same number of threads. Instead of tweaking JVM options, let’s change recursion depth in the application code. With option -Ddepth=64 recursion depth is code snippet would be limited to 64 instead of 128. \> docker run -m 512m --memory-swap 512m testapp java -XX:MaxRAMPercentage=100 -Ddepth=64 -cp /app ThreadSpawner ... Heap usage: 31M | Thread count: 919 Heap usage: 28M | Thread count: 970 Heap usage: 38M | Thread count: 1021 Now the container can survive roughly two times more threads. The reason for such behavior is that memory for thread stack is allocated lazily. JVM reserves address space for stack, but real memory allocation happens only then the memory page (usually 4 KiB) is touched by the app. JVM has other memory areas apart from heap. Even though they are not as large as heap typically, they have to be accounted for too. Key JVM memory areas are Heap - our plain Java objects are stored here, heap size can be limited with -Xmx option. Metaspace - holds class-related data structures, grows with more classes being loaded. Metaspace can be limited with -XX:MaxMetaspaceSize option. Threads - each live thread needs a stack. Individual stack size can be tuned with -XX:ThreadStackSize, but the number of threads is driven by application. Direct memory - non-heap memory available via java.nio package. Direct buffer memory pool can be limited by -XX:MaxDirectMemorySize, the default limit is equal to the max heap size. Code cache - used for binaries of JIT-compiled code. -XX:ReservedCodeCacheSize option controls maximum memory used for code cache. This is not a full list. Native memory tracking, covered later in this post, could be enabled to get a detailed memory report from the JVM runtime. Memory related things quickly become insanely complex if you are trying to build an accurate picture. Do you need to know all this just to run your web app in a container? Probably not. Allow me to summarize the takeaways. Containers have separate limits for resident memory and resident + swap memory used by the process. Different container runtimes use different ways to set this limit. JVM scales itself based on the container resident memory limit. Although the default factor is 25%, which is too small for a dedicated Java application container. -XX:MaxRAMPercentage=75 would set the factor to 75%, a more sensible starting point in that instance. JVM scales only heap space. You need to keep in mind other areas too. Pay special attention to the number of threads, especially in small containers. Once the container memory limit is breached, do not expect any exception within JVM or shutdown hooks execution, it just gets terminated. Discover recommendations on tuning the JVM for resource constrained containers in a dedicated guide. Native memory tracking Native memory tracking is a JVM feature that can help you understand your memory budget including both heap and non-heap memory spaces. Native memory tracking should be enabled on the JVM command line via -XX:NativeMemoryTracking=detail flag. Later you could use jcmd to dump memory usage reports from JVM. An example of a summary level report is below. \> jcmd 12345 VM.native_memory summary Native Memory Tracking: Total: reserved=1822547KB, committed=62203KB - Java Heap (reserved=393216KB, committed=24676KB) (mmap: reserved=393216KB, committed=24676KB) - Class (reserved=1056947KB, committed=5811KB) (classes \#735) ( instance classes \#640, array classes \#95) (malloc=179KB \#1340) (mmap: reserved=1056768KB, committed=5632KB) ( Metadata: ) ( reserved=8192KB, committed=5120KB) ( used=4825KB) ( free=295KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=512KB) ( used=454KB) ( free=58KB) ( waste=0KB =0.00%) - Thread (reserved=115662KB, committed=16334KB) (thread \#112) (stack: reserved=115128KB, committed=15800KB) (malloc=405KB \#674) (arena=129KB \#222) - Code (reserved=247772KB, committed=7632KB) (malloc=84KB \#741) (mmap: reserved=247688KB, committed=7548KB) - GC (reserved=1333KB, committed=133KB) (malloc=45KB \#159) (mmap: reserved=1288KB, committed=88KB) - Compiler (reserved=165KB, committed=165KB) (malloc=32KB \#68) (arena=133KB \#5) - Internal (reserved=4674KB, committed=4674KB) (malloc=4642KB \#1679) (mmap: reserved=32KB, committed=32KB) - Symbol (reserved=2055KB, committed=2055KB) (malloc=1247KB \#2917) (arena=807KB \#1) - Native Memory Tracking (reserved=385KB, committed=385KB) (malloc=187KB \#2643) (tracking overhead=198KB) - Arena Chunk (reserved=188KB, committed=188KB) (malloc=188KB) - Logging (reserved=4KB, committed=4KB) (malloc=4KB \#191) - Arguments (reserved=18KB, committed=18KB) (malloc=18KB \#476) - Module (reserved=59KB, committed=59KB) (malloc=59KB \#1027) - Synchronizer (reserved=63KB, committed=63KB) (malloc=63KB \#494) - Safepoint (reserved=8KB, committed=8KB) (mmap: reserved=8KB, committed=8KB) You can replace summary with detail in the command above to get an even more detailed report. JVM can also calculate memory usage difference. Use jcmd PID VM.native_memory baseline to capture baseline. Later use jcmd PID VM.native_memory summary.diff or jcmd PID VM.native_memory detail.diff to get a report of changes in memory usage. Native memory tracking is useful for investigating abnormal memory usage by JVM, but it does cover only JVM memory areas. There is still libc heap used by JVM or other native code loaded into the process, which is not accounted for by this report. Networking Networking is another part of the container isolation boundary. A container has its own loopback adapter (127.0.0.1) and one or more virtual Ethernet adapters to speak to the rest of the world. A virtual network interface of a container is typically NATed through the host’s network interface—not only an option but the most common one. NAT (Network Address Translation) is a widely used technology. But it is introduced in a very unexpected place with containers. VPN and the mystery of hanging connections One issue with NAT is the inability to process fragmented IP packets (in the case of port-based NAT). Again fragmented IP packets are rare on the network, but a combination of VPN and NAT on the same host machine (e.g., developers desktop) could introduce funny network issues. VPN may have a lower MTU than normal network and break packets into fragments. Small pieces of data would pass smoothly between an application in the container and service on the other side of VPN. Yet, once the size of data reaches a certain limit, connection hangs forever. Docker has an option to disable network isolation, which helps to work around such problems. Kernel level resource limits As the kernel is shared across all containers, certain global limits are applied to all containers too. Containers tend to be small; so, a good server could be stuffed with lots of them and exhaust these limits. The effect might be very obscure: say, the container is running fine on one node but experiencing network issues on another. JMX in container JMX is a TCP based protocol used by JVM to communicate with diagnostic tools. JMX is an ancient protocol based on RMI (Remote Method Invocation) protocol endemic to the Java world. On the JVM side, you need to add options to start listening on a particular port. Then remote tools such as Mission Control can connect to this port and start talking to the JVM. If you wonder why you would want to use Mission Control, take a look at my first article about JDK Flight Recorder. Due to the idiosyncrasies of a JMX protocol, using it in containers becomes a challenge. Above there is a sequence diagram of the JMX connection. A problematic piece is a serialized stub of a remote object with an IP address and a port the second connection would use. Below is an example of JVM options to listen to the JMX protocol on port 55555. With Docker, you would need to add port mapping with option -p 55555 to expose this port starting a container. JMX config options Connection diagram -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=55555 -Dcom.sun.management.jmxremote.rmi.port=5555 One way to fix it is to override the address inside the stub. JMX config options Connection diagram -Djava.rmi.server.hostname= -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=55555 -Dcom.sun.management.jmxremote.rmi.port=5555 This being one solution, it will not work if you cannot connect to the container’s host directly or if the port cannot be exposed with the same number. In other words, Kubernetes would need to solve the problem differently. Another solution requires forwarding. JMX config options Connection diagram -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=55555 -Dcom.sun.management.jmxremote.rmi.port=5555 Such a setup would work with SSH tunnels and Kubernetes port forwarding. Conclusion Containers solve a lot of problems related to the deployment of complex applications. Unfortunately, they also bring in a bunch of new ones. When things work, containers are great. But both software vendors and application developers have to be aware of runtime considerations specific to Linux containers. The early years of container adoption were pretty rough for JVM, as many old configuration heuristics were failing miserably in containerized environments. Thanks to the work done by OpenJDK supporters, numerous issues have been addressed. Now OpenJDK is well prepared to run in containers. Related Posts JDK Flight Recorder – a gem hidden in OpenJDK Hunting down code hotspots with JDK Flight Recorder Hunting down memory issues with JDK Flight Recorder JDK Flight Recorder, The Programmatic Way - [TeXnical Writing Part 1: Foundations](https://bell-sw.com/announcements/2020/11/02/TeXnical-Writing-Part-1-Foundations/): Introduction Plain text documents are timeless. The earliest ASCII documents ever written can still be opened, read, and modified today, on virtually any hardware, using any operating system, without any proprietary software, online service, or third-party conversion program. In stark contrast are document formats such as those produced by Microsoft Word, ClarisWorks, Lotus Manuscript, WordPerfect, WordStar, and most other word processors, whether discontinued or not. A double-edged sword to some proprietary word processors is their ability to show the document’s final form while editing. This ability makes tying a document’s appearance to its content easy, which can lead to troubles later on when attempting to change either independently. Before 1978, typesetting mathematics was expensive, laborious, or aesthetically displeasing. Eventually, a small subset of word processing software offered features to typeset formulas and equations (e.g., Microsoft Word 6.0 with Equation Editor 1.0 released in the early 1990s). While this addressed some issues, proprietary file formats remained problematic. In 1978, Donald Knuth released an open specification for typesetting mathematics using a plain text syntax called TeX (pronounced “tech”). Back then computers were not fast enough to render TeX in real-time, making it impossible to see the final form while actively editing the document. Modern laptops are powerful enough to typeset TeX in real-time, decreasing the amount of time spent waiting for documents to build. Software developers, scientists, engineers, and mathematicians all use mathematics to communicate algorithms, formulas, and equations succinctly and accurately. As technology-based societies expand, we need tools to help people express complex ideas without ties to proprietary technology. In other words, we’d like a workflow that gives us the ability to write plain text documents that may be published to any medium, such as web or print, using cross-platform software. In this series, we’ll develop a Java-based desktop application using JavaFX that converts mathematical formulas written in Markdown documents into HTML documents. We’ll conquer technical challenges using techniques that allow us to achieve the real-time conversion from Markdown to HTML. Each article in the series builds upon the previous one. Broadly, the articles include: Foundation — Create a new project, develop a text editor, and build an executable binary. Markdown — Update the text editor to support Markdown syntax, and provide a preview panel to show the result. Syntax — Add syntax highlighting to the text editor. Math — Include a TeX engine that can draw mathematical formulas on a JavaFX canvas or as scalable vector graphics, such as the following equation: Performance — Profile the application to attain real-time rendering of hundreds of formulas. To develop the editor, we’ll use Liberica JDK for a few reasons. First, it comes bundled with JavaFX, which is a feature-rich library for writing cross-platform desktop applications. Second, the way the download URLs are designed makes it super-easy to write installation scripts that target specific Java versions. Audience Readers are expected to have a working knowledge of software design patterns, the Java programming language, and be familiar with web-related technologies such as HTML and CSS. Basic knowledge of TeX is useful but not necessary. Readers should understand basic development operations, such as configuring environment variables. Although the instructions are written for a Unix-like platform (e.g., Linux), the principles behind them are applicable to any operating system. For Windows users, consider one of the following solutions: Install Cygwin Install the Windows Subsystem for Linux Install VirtualBox along with a Linux distribution Software requirements Download the latest versions of the following software packages, preferably in .zip or .tar.gz format where possible: Liberica JDK (full version) from BellSoft, such as shown in the following figure: Gradle from Gradle Inc. IntelliJ IDEA from JetBrains s.r.o. For the purposes of these instructions, we’ll refer to the download location as $(logname)/downloads, which references the non-administrative account name. On Windows, this would be equivalent to %USERNAME%\Downloads. Development environment setup This section describes the installation steps to configure the system for development. There are many ways to set up a software development environment. Installing third-party applications into /opt then leveraging symbolic links makes upgrading and downgrading a simple matter of replacing a symbolic link. This approach can work on Windows but requires special software to create directory links. Liberica JDK Broadly, we want to install Liberica JDK into a common directory, set up the JAVA_HOME environment variable, update the system PATH variable, then verify that Java is installed. Accomplish this by completing the following steps to install Java system-wide: Open a terminal. Change to the root user (e.g., sudo su -). Extract Liberica JDK using the following commands (change the archive filename as appropriate): cd /opt tar xf $(logname)/downloads/bellsoft-jdk15+36-linux-amd64-full.tar.gz Create a symbolic link having a name that is independent of the Java version installed: ln -s jdk-15-full jdk Log out of the root account. Java is now installed into /opt/jdk. Next, configure the environment variables for a non-administrative account as follows: Edit the system environment variables (e.g., $HOME/.bashrc). Append the following lines: export JAVA_HOME=/opt/jdk export IDEA_HOME=/opt/idea export GRADLE_HOME=/opt/gradle export PATH="$PATH:$HOME/bin:$JAVA_HOME/bin:$IDEA_HOME/bin:$GRADLE_HOME/bin" Save the file. Open a new terminal (you may close the previous one). Verify that Java is installed: java -version If successful, the console will display a result similar to the following output: openjdk version "15" 2020-09-15 OpenJDK Runtime Environment (build 15+36) OpenJDK 64-Bit Server VM (build 15+36, mixed mode, sharing) Java is installed. Advantages to this installation approach include: full control over the exact version of Java being installed without regards to availability by package managers; the environment variables never have to change value; switching between versions entails changing a single symbolic link; and we can ensure that the only place Java binaries and supporting libraries are located is under /opt. The main disadvantage is that some packages cannot detect that Java is installed, despite both JAVA_HOME being set and the java command being available via the PATH. There are other ways to install Java on your system, such as using a package manager. Note that you must configure the package manager to download the full version of Liberica JDK. Gradle Gradle is a build system we’ll use to simplify compiling Java source files into bytecode then bundling our bytecode and third-party libraries into a single JAR file. Install Gradle using similar steps to installing Java, as follows: Open a terminal, if not already opened. Change to the root user (e.g., sudo su -). Extract Gradle using the following commands (be sure to substitute the version number as appropriate): cd /opt unzip $(logname)/downloads/gradle-6.6.1-bin.zip Create a symbolic link as follows: ln -s gradle-6.6.1 gradle Log out of the root account. Verify that Gradle is installed: gradle -version If all went well, the console will display a result that begins with the following output, showing the version number: ------------------------------------------------------------ Gradle 6.6.1 ------------------------------------------------------------ Gradle is installed. IntelliJ IDEA Repeat similar steps to installing Java and Gradle for installing the integrated development environment (IDE): Open a terminal, if not already opened. Change to the root user (e.g., sudo su -). Extract IntelliJ IDEA using the following commands (substitute the version number as appropriate): cd /opt tar xf $(logname)/downloads/ideaIC-2020.2.2.tar.gz Create a symbolic link as follows: ln -s idea-IC-202.7319.50 idea Log out of the root account. IntelliJ IDEA is installed. Project environment setup For this project, we’re going to create a non-modular application built using Gradle. After Gradle is configured, we’ll import the project into the IDE and then begin development. Initialization Initialize the project as follows: mkdir -p $HOME/dev/java/mdtexfx cd $HOME/dev/java/mdtexfx gradle init Next, Gradle will ask a few questions; when prompted, provide the following answers: Choose 2 to build an application. Choose 3 to set the language to Java. Choose 1 to set the build script to use Groovy. Choose 4 to test using JUnit Jupiter (a.k.a. JUnit 5). Press Enter to accept a Project name of mdtexfx. Set Source package to com.mdtexfx Gradle will create the following directory structure: . ├── gradle │ └── wrapper └── src ├── main │ ├── java │ │ └── com │ │ └── mdtexfx │ └── resources └── test ├── java │ └── com │ └── mdtexfx └── resources The project is initialized, along with the following source files: App.java — contains the main application entry point; and AppTest.java — helps verify that the code in App.java is correct. Build and then run the project by typing the following commands into the terminal: ./gradlew clean build ./gradlew run The output from Gradle will contain: > Task :run Hello world. This demonstrates that we’ve successfully initialized a rudimentary Gradle application. Open project Confirm that the application can run within the IDE as follows: Start IntelliJ IDEA. If no project is currently open: Click Open or Import. Open $HOME/dev/java/mdtexfx. Otherwise, if a project is already open: Choose File → New → Project from Existing Sources. Browse to $HOME/dev/java/mdtexfx. Click OK. Select Gradle. Click Finish. Click Run → Run. Select App. The same “Hello world” output message appears, which indicates that the IDE can run the application. Synchronize with build script IntelliJ IDEA may issue false warnings or report errors that are not necessarily correct; keeping the IDE in sync with the latest changes to the Gradle configuration file can resolve erroneous warnings. Accomplish this using the following steps, from within the IDE: Open build.gradle. Optionally, remove or replace the header comment at the top of the file. Click the Load Gradle Changes icon. The IDE reloads the Gradle configuration file. Configure Gradle Now that IntelliJ IDEA can run the application, we can proceed with configuring the build script for a simple JavaFX editor. Our Gradle build scripts use Groovy syntax, though other languages are possible—like Kotlin. This entails the following coarse-grained steps: Apply a JavaFX plugin Configure the JavaFX plugin Update dependencies Create an überjar Run the application An überjar is a Java archive (.jar) file that contains all class files and resources necessary to run the application. Apply JavaFX plugin JavaFX requires native binaries for each target platform. The JavaFX plugin simplifies the work of including native binaries for a specific platform. Update the plugins section to include the JavaFX plugin: // Apply the JavaFX plugin to add support for JavaFX. id 'org.openjfx.javafxplugin' version '0.0.9' Configure JavaFX plugin After the repositories section, configure the JavaFX plugin as follows: javafx { version = '15' modules = ['javafx.controls'] configuration = 'compileOnly' } Setting the configuration option to compileOnly instructs the JavaFX plugin that the native binaries will be provided separately. This is where Liberica JDK shines because it bundles the necessary JavaFX components with its full version of the OpenJDK. Setting the modules indicates that the application requires basic user interface functionality. This includes the concept of a scene, which is fundamental for creating a user interface in JavaFX. The version setting instructs the plugin to use modules for the given JavaFX version. Update dependencies Inside the dependencies section, insert the following code: runtimeOnly "org.openjfx:javafx-controls:15:linux" Adding the given dependency tells Gradle to download the necessary native binaries for running the application on Linux. As the application grows in complexity, we’ll need to include additional JavaFX dependencies. Create an überjar After the application section but before the test section, add the following code: jar { manifest { attributes 'Main-Class': application.mainClassName } from { (configurations.runtimeClasspath).collect { it.isDirectory() ? it : zipTree( it ) } } { exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' } } By default, Gradle will create a Java archive filename using the project name (i.e., mdtexfx.jar). To customize the filename, set archiveFileName to the desired value. Setting manifest allows the application’s main entry point to be executed by passing a -jar argument when running the Java virtual machine, which we’ll see shortly. The from section instructs Gradle to combine all the Java archive files required to run the application into a single file—the überjar. Without this step, launching the application would otherwise require passing a classpath argument to the Java virtual machine that includes paths to all the JAR files necessary to run the application. Excluding *.RSA, *.SF, and *.DSA removes all signature files from the final überjar. This precaution ensures that developers can sign the final Java archive file without conflicting with existing signature files. If .pom files are present, we may have to exclude those as well. Run application First, build the application’s Java archive (JAR) file through the IDE as follows: Click Gradle, found on the right-hand side. Expand mdtexfx → Tasks → build. Double-click jar. The application is built. Next, run the application as follows: Open a terminal. Change to the project directory (i.e., $HOME/dev/java/mdtexfx). Type: java -jar build/libs/mdtexfx.jar The console shows: Hello world. An application JAR file is built. Text editor Implement a minimal text editor as follows: In the IDE, expand mdtexfx → src → main → java → com.mdtexfx. Double-click App to open it. Replace the contents with the following program: package com.mdtexfx; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.TextArea; import javafx.scene.layout.Pane; import javafx.stage.Stage;
public class App extends Application { @Override public void start( final Stage stage ) { final var editor = new TextArea(); final var scene = new Scene( editor ); stage.setScene( scene ); stage.show(); } }
Notice that our main class (App) inherits from the JavaFX class named Application. The Application class provides the scaffolding necessary to launch a standalone JavaFX program. Next, the start method is called by the Application’s launcher to wire together the components that make up our program, including: TextArea — Graphical user interface element for text editing. Scene — Container for one or more user interface elements. Stage — Top-level container, typically the application window. JavaFX uses a theatre analogy: there is only one Stage; a Stage can have many Scenes, but only one is “performed” (active) at a time; each Scene contains JavaFX widgets—such as a TextArea—that are called nodes, and they represent the visual elements displayed, much like tangible objects in a theatrical performance. Calling stage.show() causes the application’s window to open, revealing the scene, which contains a single user interface element: the text area. From the terminal, rebuild and run the application as follows: ./gradlew clean build java -jar build/libs/mdtexfx.jar The following window appears: Click into the window to give the text area focus, type some text, then close the window. The application starts, accepts input, and can be closed. Unit test Changing App.java broke the unit test in AppTest.java. Before continuing, it is good practice to make sure the unit test passes. For our purposes to this point, if the application can be instantiated without failure, we’ll consider the test to have passed. Change AppTest.java as follows: class AppTest {
@Test void test_Instantiation_NewInstance_Created() { new App(); } }
The unit test now passes. (See Roy Osherove’s unit test naming standards.) Executable binaries Building a JavaFX application for multiple platforms normally requires running jlink and jpackage to produce binaries. The problem is that that process must be run on each target platform. This means that to build a binary for OSX, the developer must own OSX. It is unreasonable to expect developers or small companies to run their build processes on every target platform. Instead, let’s create a build process for JavaFX that can target any platform from any platform—much like cross-compiling. To build standalone binaries for each target platform, we’ll use: Liberica JDK web API; and warp, a cross-platform application packager. Install warp before continuing. Build binary Building a self-contained Linux executable entails a few steps: Prepare a full Java runtime environment Create a launcher script Package the application into a standalone binary Prepare Java runtime environment Download then extract a 64-bit Linux Java runtime environment (JRE) as follows: Open a terminal. Change to the application directory (e.g., $HOME/dev/java/mdtexfx). Download and extract Liberica JDK into dist, such as: wget -q https://download.bell-sw.com/java/15+36/bellsoft-jre15+36-linux-amd64-full.tar.gz mkdir dist cd dist tar xf ../bellsoft*linux*tar.gz mv jre-15-full jre cd .. The JRE is extracted into $HOME/dev/java/mdtexfx/dist/jre. Create launcher script Create a launcher script responsible for running the application after it is extracted out of the standalone binary. Edit a new file named dist/run.sh, then insert the following contents: #!/usr/bin/env bash readonly SCRIPT_SRC="$(dirname "${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}")" "${SCRIPT_SRC}/jre/bin/java" -jar "${SCRIPT_SRC}/${paths.app}.jar" "$@" 2>&1 >/dev/null & Save the file, then finish up by making it executable using chmod +x dist/run.sh. Package application The Java Software Development Kit includes jlink and jpackage. These programs help eliminate unused dependencies when creating a JavaFX application, shrinking the size of the production executable. Unfortunately, they cannot be used to cross-build native binaries. That is, Linux versions cannot produce a standalone executable that runs on Windows, or vice-versa. One of Java’s core draws is its ability to create cross-platform software. Since JavaFX is no longer bundled with Java, operating system-specific libraries must be bundled into the Java archive file. This means creating separate native binaries for each target platform. Ideally, we’d be able to create all the native binaries from a single computer, regardless of the operating system. We can, but not with jlink and jpackage. Instead, package the application into a standalone binary using the warp-packer program as follows: ./gradlew clean build mv build/libs/mdtexfx.jar dist warp-packer \ --arch linux-x64 \ --input_dir dist \ --exec run.sh \ --output mdtexfx.bin Verify the standalone executable displays the application’s text editor by running it as follows: ./mdtexfx.bin Summary We’ve created a standalone binary for a JavaFX application, which can be cross-compiled to multiple platforms. The next article will build upon these foundational concepts to develop a Markdown text editor and a preview pane. Related posts TeXnical Writing Part 2: Markdown TeXnical Writing Part 3: Syntax TeXnical Writing Part 4: Math TeXnical Writing Part 5: Performance - [BellSoft Kicks Off Bundled Offer with Karakun for Better Software Security and Simpler Migration](https://bell-sw.com/announcements/2020/11/13/BellSoft-Kicks-Off-Bundled-Offer-with-Karakun/): BellSoft, one of the top-5 OpenJDK contributors, and Karakun AG, a software engineering company that specializes in the development of custom solutions and maintainer of OpenWebStart, an open source re-implementation of the Java Web Start technology, have recently announced a new partnership. BellSoft and Karakun collaborate to provide a safe path from Oracle Java to open source solutions. With their high expertise, both companies can offer the market, which has seen a shift towards free and supported OpenJDK-based deployments, a new product: Liberica JDK with OpenWebStart, a multifaceted and, most importantly, reliable alternative to Oracle JDK with simple migration services and minimal risk. A study on “Global Java Web Frameworks Software Market 2020–2028” names security risks to data storage a major problem for the IT industry. In line with this, both BellSoft and Karakun focus on software security, put great emphasis on the ease of switching, and ensure that their products are exceptionally reliable. “We at BellSoft develop advanced software, accumulating the best Java practices into Liberica JDK. But Java’s future does not lie in constant innovation alone. Meeting users’ needs and guaranteeing their security are what drive our actions. We want to work with companies that also treat these aspects as the utmost priority. Hence, our strategic partnership with Karakun evolved from joint work within the Java Community, highly concerned with usability, convenience, and stability. We can guarantee that customers will benefit from a seamless migration process. As we strive to provide our customers with flawless security capabilities, BellSoft is involved in creating standard specifications for Java technology under the JCP program. This year, our representatives are nominated as Candidates for the Executive Committee,” says Alex Belokrylov, CEO of BellSoft. After software security, the ease of migration is another vitally important factor. As surveys show, nearly half of Oracle JDK customers require quality cost-effective solutions with releases coming in time and ready for production. Many enterprise developers need Java Web Start (JWS), a one-click deployment solution for Java applications on the desktop. Oracle JDK supported JWS until it was deprecated in Java 9 and subsequently removed from Java 11. Karakun and BellSoft will provide expert migration services to assist companies in switching from Oracle JDK to 100% open source solutions: reliable Liberica JDK with open source OpenWebStart in one package. Besides, German-speaking customers will take advantage of first line support in German. “Besides the open source character of Java represented by OpenJDK, it is very important for us to offer professional commercial support for such a crucial component like the JVM. Our cooperation with BellSoft provides an ideal and flexible bundle for all interested groups—either with OpenWebStart or with the classic JDK,” says Stephan Huber, COO of Karakun AG. Karakun now cooperates with BellSoft to deliver a fully-fledged, regularly updated, and thus secure Liberica JDK with OpenWebStart. This new complex product will help businesses to achieve the following: migrate from Oracle Java to Liberica JDK safely and fast; find new opportunities if they rely on WebStart / JNLP; reduce software and information security risk; stay with long-term support (LTS) releases; cut down enterprise costs for licensing; get first line support for Liberica JDK German-speaking customers. Confirmations and additional information: JVM Ecosystem Report 2020 Global Java Web Frameworks Software Market 2020–2028 Download the latest JDK 15 Request support for Liberica JDK with OpenWebStart Executive Committee of Java Community Process About BellSoft: BellSoft releases and supports Liberica JDK, an OpenJDK binary distribution verified by TCK for Java SE Standard Compliance. BellSoft engineers have been contributing to the OpenJDK project since its inception. Being one of the top-5 OpenJDK contributors, BellSoft helps effectively solve business problems in the server, cloud, and IoT applications of Java technologies. The company drives a community-powered approach to deliver reliable and compact containers with Liberica JDK targets microservice solutions on. Using popular as well as in-house performance optimization techniques and tools allow BellSoft to work on performance tuning for industry leaders. It also enables the company to provide customers with the software that meets their current and future business challenges, all based on the best Java expertise. bell-sw.com About Karakun AG: Karakun is the home of 50 highly qualified individuals, with 40 very experienced software and UX experts among them. Karakun uses agile methods to develop custom-tailored solutions for companies and organizations, based on Java and web technologies. Karakun employees are very active members in numerous open source projects. As knowledge transfer is an important part of Karakun’s DNA, the know-how and ideas resulting from this community work are being used in customer projects. To increase the level of standardization and to maximize efficiency during development, Karakun provides own software platforms for dedicated use cases. karakun.com - [An intelligent bet for Kotlin Part 1](https://bell-sw.com/announcements/2020/11/16/An-intelligent-bet-for-Kotlin-part-one/): The Java industry is moving forward along with the world around it. And software companies, as part of its community, are very much in tune with the trends. Progressive and novel things often emerge from technology crossovers. Here’s why BellSoft, determined to keep you informed of everything Java-related, has invited me to make a two-parter on the young and advanced Kotlin language. If you’re a Software Developer or any kind of professional working in the IT sector, you’ve probably heard of it. Kotlin has absorbed all the best features from other languages, while keeping full Java interoperability. Among the benefits, its supporters enumerate conciseness, expressiveness, simplicity. A very flat learning curve for Java developers is also a fact to consider for some organisations before deciding if they should switch to Kotlin. In the first article, I’ll be going through Kotlin’s most useful characteristics, like object destructuring from JS, and type interference, and many more. Here you’ll see its areas of application and how the language makes the developers’ lives easier when writing code compared to others. But more importantly, in the second part, we’ll discuss async programming with coroutines, observe how the language impacts the world now and even look into its future! You will basically learn everything you need to apply Kotlin to your systems. Its history Kotlin was first released in 2011 by the company JetBrains. If you don’t know about them, they were first known for creating the popular IntelliJ IDEA. Now they’re also very well-known as the creators of the Kotlin language. Although their first release was in 2011, it wasn’t until 2016 when Kotlin v1.0 came out, the first official release from which JetBrains will provide backwards compatibility. According to different sources, JetBrains engineers started writing their own language primarily due to not finding any that fulfils their needs. The goal was to build a concise, elegant and expressive language also able to be compiled fast. Another direction is crafting code into a domain-specific language (DSL) specialized for certain parts in an app. Instead of using design patterns and building unmanageable architectural solutions from scratch, Kotlin is here to help. It features numerous applications: you can basically write your own language without parsing software, IDEs, etc. It allows replacing long chains of factory methods and code duplication with readable constructions without any external tool. Writing a new language is not easy, but writing a new language with full interoperability with an existing language like Java is even more difficult. However, despite the challenges, JetBrains seems to be doing a very good job so far, seeing how warm a welcome their language has received from the community. Applications The foundation is that Kotlin is super versatile. Regardless of its comparatively young age, it’s an archetype for generating benchmark tests with JMH on par with much more “mature” Java, Groovy and Scala. Kotlin is good for low-level profiling to assess the performance and scalability of the systems. Here’s a custom example of Kotlin-based microbenchmark: @BenchmarkMode(Mode.Throughput) // Benchmark mode, using overall throughput mode. open class MyBenchmark{ @Benchmark //Just like in Java. fun testMethod(): Int { return sequenceOf(1..10).flatten() .map{ it * 2 } .filter { it % 3 == 0 } .map{ it+1 } .sum() } } Another direction is crafting code into a domain-specific language (DSL) specialized for certain parts in an app. Instead of using design patterns and building unmanageable architectural solutions from scratch, Kotlin is here to help. Replace long chains of factory methods and code duplication with readable constructions without any external tool. Or how about configuring Gradle builds? Also possible. There’s an official plugin from JetBrains to compile Kotlin code to target JVM (apply plugin: "kotlin”), JavaScript (apply plugin: "kotlin2js”), or Android: buildscript { ext.kotlin_version = '' ... dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' As you can see, just over four years, Kotlin has become a multi-purpose ecosystem in itself. With it, you can write code, create microservices, generate JUnit tests, build microbenchmarks, and even assemble projects (which Java can’t do). Why is Kotlin becoming so popular and widely adopted in our industry? Let’s go through its key features to understand the reasons behind its popularity.1 Simplicity and pragmatism above all If there’s one thing that characterises Kotlin, this is its focus on pleasing developers’ wishes, an obsession in making their lives and their day-to-day work as smooth and simple as possible. When we write code daily at a professional level, it’s the small and apparently unimportant things that we face very often that kill our productivity. They produce a constant sense of waste of time and demotivation in us. Kotlin has removed quite a few unnecessary annoyances, very well-known and recognised by all, although for one reason or another nothing had been done until now. Now I’ll cover some of them briefly. Multiplatform Kotlin is a multiplatform language which can be executed in different supported platforms. You can target different runtime environments like JVM, Android, JS or many others with the use of Kotlin/JVM, Kotlin for Android, Kotlin/JS and Kotlin/Native. Kotlin Multiplatform, JetBrains’ open source code sharing SDK, is proven to accelerate development time across several teams who work on the same application for Android, iOS and web, up to 30%.2 It allows using a single business logic code in cross-platform software and basically taking advantage of one engineering team instead of two or three. Such a solution brings significant savings (in terms of both money and time), fewer bugs and QA processes, higher efficiency. To achieve all these benefits, JetBrains have their progressive runtime supported by BellSoft. In 2019, the two companies entered a partnership: BellSoft started providing updates and security patches in the same way it supports its own Liberica JDK. Due to Kotlin code being 100% Java-interoperable, I would say that Liberica JDK is the runtime for Kotlin. However, keep in mind that the status of the Multiplatform project is still in the alpha phase. It’s not stable yet, and you could expect some migration issues. Types Kotlin types are very similar to Java (numbers, characters, booleans, arrays and strings) with one main difference: primitives do not exist in Kotlin. Although primitives can’t be explicitly defined, Kotlin’s compiler will translate types into primitives in the JVM bytecode every time that it’s possible to favour a lower memory footprint. Creating a new instance Some languages like Java or C++ require the use of the “new” operator to create a new instance of an object. We’re all used to it, but is it indispensable? Why not just call the constructor? This is precisely what Kotlin does. We no longer need to use “new” in Kotlin, calling an object’s constructor is enough to create a new instance. For example, we can create an instance of an Order object like this: val order = Order(“id”, 10, BigDecimal(12.5), OrderType.BUY) End of statements For instance, why are we forced to write a semicolon at the end of each line in some languages? Providing the ability to write multiple statements in one single line is unnecessary and possibly not recommended for readability purposes, so why not accept a break line as the standard to represent the end of a statement? If you’re a Java or C++ developer, you’ve probably missed a semicolon thousands of times. Think about all the time wasted. Kotlin doesn’t require using a semicolon to close a statement, although if you do it, it will still compile. Type inference and immutability Kotlin also reduces the verbosity of having to declare the type of variables twice. Type inference allows us just to declare the creation of a new variable, inferring the type from the right-hand side part of the statement. Another improvement in variable declarations is that we don’t have to use any additional modifier to declare a variable as immutable. Kotlin provides just two keywords: var and val. Much simpler than in other languages, if you need a mutable variable use var; if you need an immutable variable use val, as simple as that. For instance, the following snippet would be allowed. We can override the value of a mutable variable defined with “var”. var order: Order = Order("id1", 10, BigDecimal(12.5), OrderType.BUY) order = Order("id2", 12, BigDecimal(15.5), OrderType.SELL) On the other hand, this situation won’t be allowed, as “order” is a immutable variable: val immutableOrder: Order = Order("id1", 10, BigDecimal(12.5), OrderType.BUY) immutableOrder = Order("id2", 12, BigDecimal(15.5), OrderType.SELL) //Compilation error! Data classes Kotlin provides a very neat and concise way of defining immutable objects. Here they’re called “data classes”. Declaring a class with immutable fields has always been specially over-complicated and verbose in Java, while in Kotlin you can achieve this in a single line: data class Employee(val id: String, val name: String, val age: Int) {} The NullPointerException dilemma If you’re a Java developer, you’ve definitely suffered the issues of having null as a possible value for an object or variable. What would be the simplest solution to this problem? Common sense tells us that not allowing nulls in the first place would probably be the simplest and safest solution; and that’s exactly what Kotlin does. If you try to assign a null value to an object in Kotlin, you’ll get a compilation error. So, by default, objects are not nullable, you’d have to explicitly tell the compiler that the object is nullable to allow nulls. That’s great, isn’t it? Once more, simplicity and safety first. val order: Order = null //Compilation error! val nullableOrder: Order? = null String interpolation I bet you’ve missed this feature hundreds of times. If you use a language with no support for string interpolation, having to concatenate different parts of a string to build the expected format is quite cumbersome. Kotlin provides a straightforward and fine way to do string interpolation. For instance, you could do this: val name = "John" val surname = "Smith" println("My name is $name $surname") Object destructuring Kotlin brings to us one of the coolest features available in JavaScript, object destructuring. We can do something like this: val (name, age) = Employee(“id1”, “John”, 35) Looks great, although unfortunately, this can only be done with data classes. If we wanted to use string interpolation without data classes, we are forced to implement component1() and component2() operators in our Order object, something that I’m personally not very happy with. For instance, if our Employee class weren’t a data class, we would have to do the following to be able to use object destructuring: class Employee(val id: String, val name: String, val age: Int) { operator fun component1(): String = name operator fun component2(): Int = age } You could also use Kotlin extensions to implement the required operators for the Employee class: private operator fun Employee.component1(): String = name private operator fun Employee.component2(): Int = age You must be wondering why we have to do that to be able to use object destructuring. The actual reason is that Kotlin compiler compiles a data class to the following if you decompile the corresponding class file: // IntelliJ API Decompiler stub source generated from a class file // Implementation of methods is not available package com.coding.kotlin public final data class Employee public constructor(id: kotlin.String, name: kotlin.String, age: kotlin.Int) { public final val age: kotlin.Int /* compiled code */ public final val id: kotlin.String /* compiled code */ public final val name: kotlin.String /* compiled code */ public final operator fun component1(): kotlin.String { /* compiled code */ } public final operator fun component2(): kotlin.String { /* compiled code */ } public final operator fun component3(): kotlin.Int { /* compiled code */ } } You can see that Kotlin provides a component operator for each property defined in the data class. However, if you modify Employee class not to be a data class, just a simple Kotlin class, then you will see that Kotlin doesn’t override those operators. // IntelliJ API Decompiler stub source generated from a class file // Implementation of methods is not available package com.coding.kotlin public final class Employee public constructor(id: kotlin.String, name: kotlin.String, age: kotlin.Int) { public final val age: kotlin.Int /* compiled code */ public final val id: kotlin.String /* compiled code */ public final val name: kotlin.String /* compiled code */ } One thing that Kotlin’s object destructuring solves nicely is the complex and messy manner that Java provides to iterate through the entries in a Map. In Kotlin we can simply do the following: for ((key, value) in myMap) { println("$key, $value") } This is definitely much more pleasant and still as fast! Default arguments In some languages, we have to duplicate constructors to allow the optionality of some of its arguments, something that adds clutter to our classes. Kotlin allows the option of providing default values for our arguments when they’re not specified. For instance, if we define a default value for our order type: constructor(userId: String, quantity: Int, price: BigDecimal, type: OrderType = OrderType.BUY) We can now instantiate an Order object without specifying the order type: val buyOrder = Order("id", 10, BigDecimal(13.2)) Named arguments If you’ve worked with complex domains with a considerable number of fields, it’s often difficult to keep track of what argument in the constructor corresponds to what field in the component. Kotlin solves this by providing the ability to name arguments in the constructor, this allows a more explicit declaration of arguments in a constructor, improving its readability considerably. Also, we wouldn’t have to place arguments in the same order in this case. For example: val sellOrder = Order("id", type = OrderType.SELL, price = BigDecimal(3.0), quantity = 1) In general, we have less rigidity and a more expressive and readable language. Async programming - Coroutines and Channels Kotlin takes a different approach to languages like Java to run multi-threaded applications. The way Kotlin works is very similar to what Golang does.3 Golang provides its goroutines and channels in the same way that Kotlin provides coroutines and channels. Kotlin coroutines are very lightweight components used to run tasks within a given scope. You can think of channels as pipes used to communicate between coroutines to achieve a common goal safely. The main benefit of Kotlin’s approach is that writing asynchronous code becomes much simpler. We’ll go through the reasons behind it in our next article in the series. Kotlin’s coroutines and channels are definitely a fascinating topic that probably deserves a sole article, but we’ll leave it here for the time being in this introductory piece on Kotlin. I’m going to give a more extended introduction to them in Part 2 of this small series. One last thing to mention is that Kotlin has some handy libraries to help us develop asynchronous applications, such as Ktor, an asynchronous framework for microservices and web applications in general.4 We could definitely go on for much longer showing all the existing features that Kotlin has come up with. But this is probably enough to give you a good idea of the radical change of mindset that this language has brought to the JVM world. A language design driven by developers, focused on productivity, simplicity and comfortability to make us enjoy what we do. A battle against boilerplate code and unnecessary clutter. This is the kind of pragmatism that Kotlin is aiming for, minuscule things that may seem insignificant by themselves. But when put together, they make a huge difference in terms of productivity! Tune in next time… As you could see after this introduction to Kotlin language, it’s a language designed with pragmatism and conciseness always in mind. Instead of reinventing the wheel with a completely new language, its developers have basically taken the nicest features from different languages in order to build one of the most beloved languages nowadays — a very sensible approach that guarantees success. Why make up something new when you already know what people love in other languages? The result is a language that keeps all the good parts of Java but simultaneously brings a more flexible and pragmatic approach that makes our day-to-day work as developers much more enjoyable. If you ask me, I think that JetBrains’ team has demonstrated that they know what developers like and what they don’t, a knowledge probably acquired from their experience building the most popular IDE. In my next article, we’ll be digging in a bit more into a few more interesting Kotlin features and discuss how Kotlin is currently doing in the market. We’ll also be talking about one very exciting aspect: what future lies ahead for Kotlin as a programming language. References Kotlin Language Guide. How Kotlin Multiplatform helps reduce app development time. The Go Programming Language. Ktor: Build Asynchronous Servers and Clients in Kotlin. Related posts An intelligent bet for Kotlin. Part 2 - [An intelligent bet for Kotlin. Part 2](https://bell-sw.com/announcements/2020/12/04/An-intelligent-bet-for-Kotlin-part-two/): The promising Kotlin language has made many developers jump for joy and even rekindled their interest in studying new ways in programming. Staying on top of all things IT, we like to look into what’s working now and how it can benefit the whole community. In the first part of this series, we went through some of Kotlin’s main features, remarking the conciseness, simplicity and pragmatism as bright signs of its identity. We touched upon its various uses, showcasing Kotlin’s versatility and how it provides plenty of options for developers. Throughout this article, we will discuss some of these features more extensively to have a better understanding of the real benefits that Kotlin could bring. We’re going to see the current impact of Kotlin on the modern industry and what we can expect in the future. Kotlin and Java: more on features Let’s dive now into a more in-depth comparison between Kotlin and Java to learn its current state and some of the problems that it might solve. If you check Kotlin’s releases page 1, you’ll see that it is on version 1.4. So how are Kotlin and Java releases juxtaposed? Are they similar or has something changed recently? Let’s compare their release cycles in the next timelines. As you could see in the first timeline, after JDK 9, JDK switched from a feature-based release cycle to a date-based one, with a new release every six months independently of the features’ state. In addition to that, a new LTS version is released every three years. Kotlin releases also first came out on a feature basis, until JetBrains decided to switch to a date-driven cycle starting from version 1.5 planned for spring 2021. It means that from 2021, both languages will be making more frequent releases regardless of whether new features are ready or still in progress. If we look closely at the language features, we can notice that Kotlin has supported from the very beginning most of the features that Java is currently trying to bring onboard. Another aspect is that Kotlin now seems to be stable and mature as a language: its releases are more focused on improving performance and developer experience. Java seems to be slowly introducing some features that Kotlin included since its early days; something that I’m calling the “Kotlinization of Java.” After looking at its main features last time, we could also see that some characteristics are similar to Java. A short learning curve makes Kotlin a good choice for Java developers. Although these two languages are often close, Kotlin repurposes certain nice features like object destructuring from JavaScript or coroutines and channels from Golang, becoming an enhanced programming language by taking all the right parts from different sources. So, the timelines affirm that Kotlin is paving the way in many aspects, as it has a better understanding of what developers demand from their primary language. In the first part of this series, I briefly mentioned Kotlin’s coroutines and its different approach for asynchronous programming with respect to Java. Let’s go through it more extensively to understand the big improvements that it brings. Async programming - Coroutines and Channels I’ve already mentioned this topic previously, but here we will try to understand why coroutines are so significant. First, let’s discuss the way Java threads work now. The current implementation of Java threads relies on the OS kernel threads. Two challenges arise: Threads are very heavy. Mainly because when a thread is suspended, both its native and Java’s call stacks have to be stored or retrieved. The OS scheduler can’t differentiate threads to group them per user or unit of work. Two threads processing the same data will most likely be executed by different CPUs. As a result, context switching between threads is expensive and degrades performance due to the high costs of transferring data between CPUs. Ideally, we’d want those two related threads to be processed by the same CPU, but that’s impossible using the OS scheduler. That brings us to Kotlin’s solution to this problem: coroutines. Kotlin’s coroutines are lightweight components used to run tasks concurrently. Compared to Java Threads, they are tiny in size and managed by Kotlin’s runtime rather than by the operating system. It means we can create lots and have a “thread” per user or transaction, in contrast to Java and its limitations. How about a coroutines example to understand them better: val start = System.nanoTime() runBlocking(Dispatchers.Default) { (1..2_000_000).map { index -> launch { delay(100) println("I'm a lightweight thread number $index! running on ${Thread.currentThread()}") } } } println("Done in ${(System.nanoTime() - start) / 10e09} seconds!") In this example, we are creating 2 million “threads,” each simulating a delay of 100 milliseconds. They will run concurrently, handled by Kotlin’s runtime. Kotlin allows a huge number of threads with no problem at all. This is absolutely not doable in Java. You’d need a limited size thread pool and reuse threads for different invocations as they’re expensive to create and to keep in memory. Running the code above, we can see that it completes successfully and the heap usage is quite reasonable considering that we’re instantiating 2 million threads: If we take a look at the actual threads running on our JVM, we can see that despite having 2 million virtual threads, the runtime is using a pool of eight threads or dispatchers. The way coroutines work simplifies writing asynchronous tasks for developers considerably. First, thread management is hidden behind Kotlin’s runtime, and second, you can write async tasks the same way you do synchronous ones. The language also provides channels as a way of communication between coroutines. Imagine them as a messaging system: An emitter sends messages to a channel, while one or more receivers process those messages. Coroutines always within a channel. It’s a much more efficient and more straightforward approach to deal with concurrency and parallelism. Instead of sharing resources, the stakeholders communicate between them to achieve a common goal! Now that we have a better knowledge of Kotlin as a language let’s see how it is currently doing in our industry. Its present, a really good start Judging from the market trends and developers’ comments on social media, the community has received Kotlin with a warm welcome. This language has been a breath of fresh air for many; its radical and intelligent approach has made a big difference. The image below shows the worldwide interest in Kotlin, according to Google Trends: Kotlin looks like a completely new language but at the same time has full Java-interoperability and is able to use the JVM as its runtime environment: this is a huge advantage and a smart move for its creators. Among different JVM languages capable of interoperating with Java code, Kotlin’s flawless interoperability is far superior in some cases. When the level of interoperability is similar, other characteristics tilt the balance in favour of Kotlin. Most enterprises are afraid of big changes. Understandably, they don’t want to get stuck in an uncomfortable situation where every employee dislikes the novelty, the management regrets the decision entirely, and no one sees an easy way of going back. Kotlin has allowed companies to take small steps, keep using the same libraries and frameworks and slowly move to Kotlin. They can see how things go following a more conservative approach, with no rush and minimal risk. Although Kotlin allows switching from Java in a smoother manner, it’s worth mentioning that transitioning to a new language in production is never an easy task. You’ll need to set up an appropriate plan with clearly defined goals. It’s crucial to understand: what we want to achieve, why we are making a choice we’re making, and how to take everyone on board, including the management. Think about what may hinder the migration. These obstacles include a potential lack of experience with the new language or difficulties in bringing new developers into your team. Once the risks are clear, it’ll be easier to prepare, minimise them or even have a backup plan ready. One important aspect to consider when migrating to Kotlin is that it has become a first-class citizen in the Spring framework after Pivotal (now part of VMware Tanzu) decided to start supporting Kotlin since version 5.0 in 2017.2 This allows using Kotlin features in the Spring integration and experiencing a simpler transition from Java for all projects using Spring. Another big step in Kotlin’s adoption has been its tremendous success in the mobile app industry. In 2017, Google first announced the Kotlin support for Android development. Then, only two years later, the IT giant publicly stated that its new preferred language for Android development was Kotlin. It means that now everyone can use the same language and the same libraries for backend and mobile applications as we do with Java, but writing a more concise, expressive and elegant code, while also taking advantage of the brilliant language features such as null-safety or coroutines. As we can see, Kotlin’s present looks bright, but what future lies ahead? Its future If someone made me bet for one language in the next few years, it’d definitely be Kotlin. Visionary language In our sector, being agile and fast to stay ahead is key to achieving business goals. That’s why betting for Kotlin could become a competitive advantage. The adoption of Kotlin could mean a boost in productivity for the teams in your organisation. Why wait for the next Java LTS release to be able to use some features which are already available in Kotlin? To make things worse, some of the features that we’ve been really missing for years in Java have been supported by Kotlin for years. While they are not even planned in Java’s roadmap for the next releases. Good examples of these missing features could be Java records: still feature previews, meaning we’ll have to wait at least 6–12 months before using them safely. Or the much-awaited Project Loom. Java Threads have been a pain for a very long time, and still, the work only started in late 2017. Although the OpenJDK community is making good progress and has early-access builds3 available, the project is still far from complete. In contrast, JetBrains managed to have something production-ready in a much more reasonable timeframe. This is one of the reasons why some developers lose their patience with Java and switch to Kotlin. When writing code in Kotlin, we get all the system benefits of Java as a JVM language, e.g. in terms of performance. Simultaneously, we benefit from one of the nicest syntaxes when it comes to readability. Usability We all know that Kotlin’s main uses are server-side and Android development. But what makes Kotlin even more interesting is that it’s been designed as a multiplatform language in a way that goes beyond our traditional understanding of “multiplatformity” from a JVM-world point of view. We can make use of Kotlin/JS to transpile our code to JavaScript, being able to write web frontend applications or even React applications!4 Without going into too much detail, I think that this could bring many benefits to full-stack development and, above all, fill an existing backend–frontend gap and improve things looking forward. Besides, we have Kotlin/Native to write Kotlin applications on the platforms where running a JVM is not possible or desirable (embedded devices, iOS, etc.) — which may be valuable to specific use cases. All of this provides increased flexibility for developers, with the added extra of fantastic tooling support provided by JetBrains, a major tooling manufacturer. Creators and community But the reasons behind a promising future for Kotlin are not purely technical; they’re also structural and organisational. From my perspective, JetBrains is a fairly small, dynamic and fast-paced company with clear objectives and huge ambitions. It is able to gain the right traction and enough speed to make things happen quickly, which is something big enterprises find very hard to beat. It might be their size that makes them slower or fear of the new, but they can’t compete with JetBrains. What I see happening in the near future is that Kotlin will keep coming up with features and innovation that will improve the language experience even further. The more developers embrace it in the Open Source community, the faster changes in the JVM world will take place. JetBrains has proven with their success with IntelliJ IDEA, becoming the favourite IDE among developers worldwide, that they understand developers and their needs. It is and will be the cornerstone in their success with Kotlin as well; know your audience, and they’ll love your product. We’ve also seen that the number of companies interested in Kotlin or partnering with JetBrains has been growing in the last few years. For instance, BellSoft and JetBrains agreed a strategic collaboration in 2019, in which Bellsoft provides security patches and critical updates to JetBrains runtime, a fork of JRE used in all language IDEs. These are the same updates and the same process as for BellSoft’s own distribution called Liberica JDK. That’s one of the reasons why I’d recommend using Liberica JDK with Kotlin due to their close relationship and collaboration. I can only see this kind of trend of mutual collaborations become more active and intense in the following years. Conclusion In summary, the future of Kotlin will depend on how willing and open-minded the community is to bet for this language firmly. Considering its simplicity and full interoperability with Java, I don’t see an apparent reason not to give it a chance. Kotlin is a language able to improve a team’s productivity and the readability of the codebase. On top of that, it allows migrating gradually and taking minimal risk. In terms of growth, the dynamic and fast-paced environment provided by JetBrains could be a critical factor in the upcoming years. The company knows developers’ needs and has proven that by building the most popular IDE. This vital experience, combined with a clear determination, might be a decisive factor for a big success of Kotlin in the long run. Who knows, it might be time to break with the past and bet for the future after all! References Kotlin Releases. Introducing Kotlin support in Spring Framework 5.0. Project Loom Early-Access Builds. Get started with Kotlin/JS for React. Related posts An intelligent bet for Kotlin. Part 1 - [TeXnical Writing Part 2: Markdown](https://bell-sw.com/announcements/2020/12/10/TeXnical-Writing-Part-2-Markdown/): Welcome back to developing a Liberica JDK-based app for real-time conversion of mathematical formulas from Markdown to HTML. If the first part of this series was all about building an executable binary, the second one focuses on building a text editor that supports Markdown syntax and a preview pane to show the result. Introduction In The Pragmatic Programmer, Hunt and Thomas offer a fundamental software development principle: "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." Often touted as the DRY principle (don’t repeat yourself), I liken the principle to computers abhor exceptions. That is, introducing differences adds complexity, which compounds over time, leading to systems where the burden of maintenance exceeds the value returned from development efforts. Systems that are easy to maintain implement functionality in general terms rather than hard-code specific behaviour. Well-crafted software minimizes complexity by identifying generalizations of major components before development begins. Many general-purpose solutions to common design problems can be found in Design Patterns by Gamma et al. Markdown is a specific instance of a plain text markup syntax that provides formatting hints for a computer to use when presenting a document. Software editors for drafting and previewing plain text documents typically support a single markup format, rather than the general case. Other plain text markup formats include AsciiDoc, MediaWiki, reStructuredText, and Textile. Although supporting multiple formats is beyond the scope of this series, we’ll apply the chain-of-responsibility design pattern to keep open the possibility of doing so. The following figure shows a high-level diagram for typical Markdown editors: A more general form is shown in the following figure: Notice how the high-level components are similar to the previous diagram: a text editor, a text processor, and an output format. For our proposed editor, the text editor contains a text string that is fed through some machinery that produces another string. By leveraging the aforementioned design pattern, our text editor can edit more than merely Markdown. We’ll see how using design patterns results in software that is easily extended to add more source document formats. Consider the following diagram: In the first processor chain, a Markdown document is converted directly to HTML. That will be our focus. In the second processor chain, a MediaWiki document references variables that are first interpolated to produce a document containing the resolved interpolated values, which is subsequently converted to HTML. As we’ll see, the effort to develop a general solution is on par with a Markdown-specific solution. You may wish to download the finished files, found at the end of the article. Processors The application is fueled by text processing, so we’ll develop the core software components first, as follows: Processor — Defines how objects chain together to transform documents. ExecutorProcessor — Provides a base processor chain implementation. ProcessorFactory — Creates processor chains capable of transforming a given document type into its final format. MarkdownProcessor — Transforms Markdown documents into HTML. Let’s build these out. Create Package Begin where we left off by creating a new package as follows: Start IntelliJ IDEA. Open the mdtexfx project. Expand mdtexfx → src → main → java → com.mdtexfx in the Project panel. Right-click com.mdtexfx. Click New → Package. Set the value to: com.mdtexfx.processors. Press Enter to create the package. The package, where we’ll organize our processor-related classes, is created. Processor Create a new interface in the processors package as follows: Right-click processors in the Project panel. Click New → Java Class. Set Name to: Processor Double-click Interface to accept its creation. Change the interface definition to the following: package com.mdtexfx.processors; import java.util.Optional; import java.util.function.UnaryOperator; public interface Processor extends UnaryOperator { default Optional> next() { return Optional.empty(); } } The key points are that: the generic type T will be a String in our editor implementation; the UnaryOperator communicates that we want to perform some operation—a string transformation—on an input value to compute some output value; and returning the sentinel value of Optional.empty() denotes that, by default, each processor is a terminal link in the chain. Using an empty sentinel avoids introducing a null reference. ExecutorProcessor The application’s engine is the ExecutorProcessor class. Create it in the processors package having the following naïve definition: package com.mdtexfx.processors; import java.util.Optional; public class ExecutorProcessor implements Processor { private final Processor mNext; public ExecutorProcessor( final Processor successor ) { mNext = successor; } @Override public T apply( final T data ) { Optional> handler = next(); T result = data; while( handler.isPresent() ) { final Processor processor = handler.get(); result = processor.apply( result ); handler = processor.next(); } return result; } @Override public Optional> next() { return Optional.ofNullable( mNext ); } } Although this works, the loop that transforms the data does not significantly improve upon its equivalent pre-Java 8 implementation: while( handler != null ) { result = handler.apply( result ); handler = handler.next(); } Replacing the null sentinel with an Optional one was a good first step. We can go further by eliminating the call to handler.get(), which will be deprecated in the future. (To understand the reason for its deprecation, read the mailing list post by the method’s author, Brian Goetz; for alternatives, see JDK-8140281.) Change the method and add a new inner class as follows: @Override public T apply( final T data ) { final var result = new MutableReference<>( data ); Optional> handler = next(); while( handler.isPresent() ) { handler = handler.flatMap( p -> { result.set( p.apply( result.get() ) ); return p.next(); } ); } return result.get(); } private final class MutableReference { private T mObject; MutableReference( final T object ) { set( object ); } void set( final T object ) { mObject = object; } T get() { return mObject; } } Note these effective changes: using a final MutableReference instance satisfies the lambda expression, p -> { ... }, which requires final or effectively final variables; calling handler.get() is now handled implicitly whereby the lambda expression receives the target Processor instance using the variable p; and invoking flatMap executes the lamba expression, which transforms the input using the current processor then returns the next Processor link in the chain. Implementing the loop using the Stream class is another possibility, but be sure to measure the performance. This particular chain-of-responsibility pattern doesn’t lend itself to parallelization, so any such anticipated performance gains are nebulous as best. Similarly, using an AtomicReference class would be less code at the expense of poorer performance and possible misunderstanding over use of a concurrent class. To prove this, we tested a few different scenarios, which we named as follows: Mutable reference — The baseline apply method presented above. Atomic reference — Replaces the MutableReference with an AtomicReference. Optional get() — Swaps the flatMap call for handler.get(). Recursion — Applies the processors’ transformations recursively. Stream iterator — Invokes Stream.iterate with a flatMap and reduce call. The following table lists the highest scoring trial run (of three runs) for all implementations, against different numbers of processors in the chain: Benchmark Name Processors Operations per μs Mutable reference 2 49.447 Mutable reference 4 24.493 Mutable reference 8 8.981 Mutable reference 16 6.018 Mutable reference 32 2.686 Mutable reference 64 1.539 Atomic reference 2 24.409 Atomic reference 4 14.065 Atomic reference 8 7.527 Atomic reference 16 3.752 Atomic reference 32 2.112 Atomic reference 64 1.054 Optional get() 2 37.256 Optional get() 4 21.24 Optional get() 8 10.851 Optional get() 16 6.299 Optional get() 32 2.645 Optional get() 64 1.511 Recursion 2 27.009 Recursion 4 15.298 Recursion 8 7.469 Recursion 16 3.332 Recursion 32 2.251 Recursion 64 0.952 Stream iterator 2 6.672 Stream iterator 4 7.964 Stream iterator 8 4.405 Stream iterator 16 1.817 Stream iterator 32 1.138 Stream iterator 64 0.701 We tested the performance using the Java Microbenchmark Harness. All runs produced the same overall result: streams perform poorly, having the slowest number of operations per microsecond; and using an object wrapper yields an optimal solution for our generic processor chain. ProcessorFactory Different source documents have different processing chains. The responsibility of mapping document types to specific chains falls to the ProcessorFactory class. In the same processors package, create a new class using the following content: package com.mdtexfx.processors; public class ProcessorFactory { public static Processor create( final ProcessorContext context ) { final var successor = new HtmlProcessor( context ); final var processor = switch( context.getMediaType() ) { case UNDEFINED -> new IdentityProcessor( successor, context ); case TEXT_MARKDOWN -> new MarkdownProcessor( successor, context ); }; return new ExecutorProcessor<>( processor ); } } There’s a lot going on here and work to do before the code will compile. Broadly, the ProcessorFactory class introduces a few new concepts, including: ProcessorContext — Guards against parameter explosion when creating Processor instances. Different processors have distinct requirements; when requirements change for individual processors, we want to avoid changing ProcessorFactory, in accordance with the single-responsibility principle. IdentityProcessor — Avoids creating a special case when the input format is unknown. The processor’s name borrows from the identity function because its transformation returns the input document without modification. See the download section for the implementation. MediaType — Uses codes defined by the Internet Assigned Numbers Authority (IANA) that describe file formats and format contents. We know we’ll need to associate files with processor chains; leveraging standard definitions permits decoupling knowledge about file name extensions from the ProcessorFactory. Further, it enables writing ProcessorFactory unit tests without having to provide java.io.File instances. This leaves us with implementing the MarkdownProcessor and HtmlProcessor. Gradle Before we can implement the MarkdownProcessor, we need to instruct the compiler where it can find various libraries. Update build.gradle by changing the dependencies section to reference Vladimir Schneider’s efficient and highly configurable flexmark-java library; since we know we’re going to need the WebView class, update the javafx section to include the javafx.web module. Together, both sections should resemble the following: javafx { version = '15' modules = ['javafx.controls', 'javafx.web'] configuration = 'compileOnly' } dependencies { def v_commons_io = '2.8.0' def v_flexmark = '0.62.2' def v_junit = '5.6.2' runtimeOnly "org.openjfx:javafx-controls:${javafx.version}:linux" implementation "commons-io:commons-io:${v_commons_io}" implementation "com.vladsch.flexmark:flexmark:${v_flexmark}" testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${v_junit}" } The above snippet applies the DRY principle in that the version numbers for third-party libraries are defined but once. MarkdownProcessor If the ExecutorProcessor is the application’s engine then the MarkdownProcessor is a wheel. With the infrastructure complete, implementing a processor that converts Markdown documents to HTML is straightforward. We need to include a Markdown parser and then implement the MarkdownProcessor class. Create the MarkdownProcessor class using the following code: package com.mdtexfx.processors; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.*; public class MarkdownProcessor extends ExecutorProcessor { private final IParse mParser = Parser.builder().build(); private final IRender mRenderer = HtmlRenderer.builder().build(); public MarkdownProcessor( final Processor successor, final ProcessorContext context ) { super( successor ); } @Override public String apply( final String markdown ) { return mRenderer.render( mParser.parse( markdown ) ); } } The work we did up-front has paid off with the simplicity so afforded. Developing an AsciiDoc processor or introducing our own custom processors now has an easy-to-follow template. Integrating flexmark-java introduces the following concepts: IParse — Parses a string containing Markdown into an abstract syntax tree (AST) represented by a node. IRender — Renders a node into an HTML document, represented by a string. We may be concerned about the performance impact of transforming a Markdown document into an AST—a type of document object model—only to rebuild the HTML from a string into its own document object model. As we’ll see, this is not a bottleneck. Next, we’ll implement the processors’ context and HTML processor, then wire everything up in the main application. ProcessorContext Recall how the factory design pattern helped us create a processor capable of transforming a media type (e.g., MediaType.TEXT_MARKDOWN) into the desired output format. Another common creational design pattern is the builder design pattern. Builder classes are useful when we want to create a new instance of a class that has many possible configurations. We’ll forgo the builder pattern for the ProcessorContext class because, at present, it takes only two parameters, implemented as follows: public class ProcessorContext { private final MediaType mMediaType; private final HtmlRenderer mHtmlRenderer; public ProcessorContext( final MediaType mediaType, final HtmlRenderer htmlRenderer ) { assert mediaType != null; assert htmlRenderer != null; mMediaType = mediaType; mHtmlRenderer = htmlRenderer; } MediaType getMediaType() { return mMediaType; } HtmlRenderer getHtmlRenderer() { return mHtmlRenderer; } } Accessor methods can be considered poor design because they violate information hiding by exposing internal data to calling classes. This means that if a class changes its accessor method signatures, then its calling classes will likely have to change as well, which is contrary to the single-responsibility principle. As a compromise, the accessor methods have been declared package protected to restrict their use to only classes within the same package, limiting the scope of ripple effects that may be caused when refactoring the class. When data classes and sealed types become available, the ProcessorContext class would be suitable candidate for rewriting as a record, which is a restricted form of a class. MediaType For the ProcessorContext class to compile, it needs MediaType to be defined. We could import a library that defines a Markdown media type constant, but for the two constants we need, importing an entire library is excessive. Instead, add the following enumeration to a new package named com.mdtexfx.io: public enum MediaType { UNDEFINED( "" ), TEXT_MARKDOWN( "text/markdown" ); private final String mMediaType; MediaType( final String mediaType ) { mMediaType = mediaType; } public String toString() { return mMediaType; } } The reason for placing it in the .io (input/output) package is because in the future we’ll want to associate file name extensions with media types. HtmlProcessor Like the MarkdownProcessor before it, the processor infrastructure investment makes writing the HtmlProcessor class a quick endeavour: package com.mdtexfx.processors; import com.mdtexfx.html.HtmlRenderer; public class HtmlProcessor extends ExecutorProcessor { private final HtmlRenderer mHtmlRenderer; public HtmlProcessor( final ProcessorContext context ) { mHtmlRenderer = context.getHtmlRenderer(); } @Override public String apply( final String html ) { return mHtmlRenderer.render( html ); } } Here we’ve opted to use an as yet undefined HtmlRenderer interface that exposes a String render( String ) method signature. This approach allows the implementation details regarding how the HTML document is exported to be controlled by the class that calls into the ProcessorFactory, rather than the ProcessorFactory itself. One disadvantage is that it means an additional class and interface. A big advantage is that our unit tests won’t need to provide a graphical user interface to verify the HTML output. HtmlRenderer & HtmlPreview For the HtmlProcessor to function, it needs an implementation-independent mechanism to display HTML. By using an interface, we abide by the guideline of “program to an interface, not an implementation,” a design principle set out and described at length by Erich Gamma, one of the Design Patterns authors. The HtmlRenderer class defines a single method that hardly deserves mention: package com.mdtexfx.html; import javafx.scene.Parent; import javafx.scene.web.WebView; public interface HtmlRenderer { String render( final String html ); } The HtmlPreview class is where we finally cut some code of interest: package com.mdtexfx.html; import javafx.scene.Parent; import javafx.scene.web.WebView; public class HtmlPreview extends Parent implements HtmlRenderer { private final WebView mView; private static final String HTML_PREFIX = "" " < !DOCTYPE html > < html lang = 'en' > < head > < title > < /title> < /head> < body > "" "; private static final String HTML_SUFFIX = ""; private static final int HTML_PREFIX_LENGTH = HTML_PREFIX.length(); private final StringBuilder mHtmlDocument = new StringBuilder(65536); public HtmlPreview() { mHtmlDocument.append(HTML_PREFIX); mView = new WebView(); getChildren().add(mView); } @Override public String render(final String html) { mHtmlDocument.setLength(HTML_PREFIX_LENGTH); mHtmlDocument.append(html); mHtmlDocument.append(HTML_SUFFIX); mView.getEngine().loadContent(mHtmlDocument.toString()); return html; } } Here are the main items to notice: Text blocks. We leverage syntax that allows for multi-line strings, which are blocked between triple double-quotes, “””. HTML header. As a minor optimization, the HTML header is isolated and reused. GUI widget. The HtmlPreview class extends from Parent so that the App class can treat its HtmlPreview instance like any other JavaFX widget. This avoids having to use delegation while also completely encapsulating the WebView class. Law of Demeter violation. Ideally, our code would be oblivious to WebEngine in that we could call mView.setContent( html ) directly. Calling it via WebEngine means that our HtmlPreview class is coupled to WebView internals. This coupling, forced by the WebView API, violates the principle of least knowledge. App We can finally wire up the main application to provide a real-time preview of a Markdown document while being edited. Revise the start method of the App class to the following: @Override public void start( final Stage stage ) { final var editor = new TextArea(); final var preview = new HtmlPreview(); final var context = new ProcessorContext( TEXT_MARKDOWN, preview ); final var processor = ProcessorFactory.create( context ); final var border = new BorderPane(); border.setLeft( editor ); border.setRight( preview ); editor.textProperty().addListener( ( c, o, n ) -> processor.apply( n ) ); final var scene = new Scene( border ); stage.setScene( scene ); stage.show(); } The key changes are: Introduce a BorderPane having a left-hand editor and right-hand preview. Create a Processor based on a hard-coded Markdown media type. Add a lambda expression that is called every time the text editor changes, where c, o, and n represent the ChangeListener, old editor text, and new editor text, respectively. Invoke the processor chain to update the HTML preview node. Using the final modifier liberally communicates to readers that the variables’ value are not intended to change. Languages such as Kotlin and Scala introduced keywords to make immutable variables first-class citizens. Scala’s documentation notes that variables should be final (“val”) by default to make code more like algebra and lean towards immutable systems. More immutability implies fewer issues that can arise from otherwise complex state interactions. Some developers suggest that final adds clutter, while others have expressed that it reduces cognitive load. Regardless of coding style, when the program is run it produces a plain HTML version of the Markdown document being edited. Here’s a screen shot that shows the editor previewing the introductory text: Download You may download the complete project. Summary We’ve described a number of software development techniques, including: programming to interfaces; the factory method design pattern; the chain-of-responsibility design pattern; the Law of Demeter; the single-responsibility principle; and the DRY principle. Additionally, we developed a maintainable code base, accomplished in part by: avoiding null assignment statements; few conditional expressions; restricting accessor method scopes; and lots of immutable (final) variables. The next article will add syntax highlighting to the text editor. Related posts TeXnical Writing Part 1: Foundations TeXnical Writing Part 3: Syntax TeXnical Writing Part 4: Math TeXnical Writing Part 5: Performance - [Coding Languages for Fintech: How Will JVM Make You Succeed?](https://bell-sw.com/announcements/2021/01/05/Coding-Languages-for-Fintech/): BellSoft is rounding up 2020 with top languages for Fintech enterprises. Whether you’re a startup launching your very first MVP, an up-and-coming business, or an established player in finance, trading, or retail in search of an innovative edge, this list is guaranteed to answer all your IT needs. Here we’ll explain why you should choose JVM languages over platform-specific native ones, look into their key features and rankings. We’ll be paying special attention to how they are applied to use case scenarios, covering everything from finance to investment to banking to insurance. This list will help you understand the advantages and opportunities each one offers. You can follow our advice when selecting the programming toolkit best suited for your project. Introduction For this comparative study, we’ve collected data from various sources that rank languages by different parameters: the TIOBE Programming Community Index, PopularitY of Programming Language Index (PYPL), JetBrains, Stack Overflow and GitHub developer surveys. Our research only covers 2020 so that you get the most relevant industry profile — since being current is vital in Fintech! But first, we’d like to explain our focus for this article. Many would say that Python is best for a financial technology company (such as modern hedge funds) thanks to its developer-friendly nature,adaptability, and opportunities for machine learning. While that may be true, it lacks certain quantitative benefits for high risk businesses in Fintech. Historically, it’s Java that has been widely used by banking and financial institutions that view data security, productivity, and stability as their priorities. One of its important features for programming since the beginning in 1995 has been object-oriented nature and platform independence, which came with the Java Virtual Machine (JVM). The JVM abstracted away from the low-level physical hardware and operating systems. With this efficient way of executing programs, developers in Fintech, finance, and banking did not need to focus on object lifecycle management or other low-level complications. Instead, they were able to direct all their efforts to the business logic. Over the years, JVM has improved significantly in terms of garbage collection, performance, and maturity. Programming language designers realized the potential of this runtime. And thus many prominent, mainstream, and even newer technologies have developed with the features and immense class library both provided by JVM. Every financial enterprise can now find a solution that would meet their needs. There was slight uncertainty within the IT community regarding the JVM status when Oracle introduced the subscription-based licensing for JDK in July 2018. Fortunately, the JDK spec is still license-free and open source, thanks to the OpenJDK initiative. Several companies are now providing their own open source Java SE derivatives, including BellSoft. Its Liberica JDK is one of the leading runtimes on the market with optional enterprise support for Fintech and other industries. Support is recommended to make the most of your OpenJDK investment. Now that we have the history covered, it’s time for our piece de resistance: a list of the best JVM languages for the financial technology sector: Java, Kotlin, Groovy, Scala, and Clojure. You will not find it too difficult to guess the first one… 1. Java Java is the primary choice for business applications. It was the first to conceive and widely accept the “Write once, run anywhere” paradigm focusing on developer productivity and ergonomics. It paved the way for the success of others like Python, JS, and Kotlin. It has adopted many pragmatic concepts, algorithms, and operations like memory model, intermediate representation, multithreading and handling functions as first-class citizens. One of the most disruptive and influential programming technologies to date, Java has forever changed the industry. Let’s look into why it is now almost as successful as it was 25 years ago: It has holistic backward compatibility, which is a highly valued feature, especially in finance and banking. The programs written two decades ago still run in the latest JDK. It is statically typed and supports multiple paradigms (object-oriented, functional, imperative). Because of its massive adoption in the industry, whether it is commercial software, an Android app, big data application, or an IoT device, this technology possesses one of the richest and powerful ecosystems, having many libraries and frameworks in all domains (for instance, it may use SQL for complicated modeling to search for relations between stock prices). It is the “lingua franca” in data-intensive applications and for most of the industry’s leading DIAs (e.g., Hadoop, HDFS, Solr, Lucene, Flink, Cassandra), as mentioned in my blog Towards Data Science. Widely used in the Fintech sector, by tech giants and startups alike, Java is here to stay. Stable and reliable N26, a German neobank offering its services in the Single Euro Payments Area and the US, uses RxJava to build Android applications.1 It provides a standard workflow to manage all data and events across the application. That’s the reason behind its rising popularity among web developers. The engineering team achieved clean separation of concerns, which made the features easier to navigate in code. Take a look at a short finance-related snippet from the app’s feature to display a list of the bank user credits drafts: Flowable<Option<List<CreditDraft>>> getAllCreditDrafts() { return store.getAll(); } Completable fetchCreditDrafts() { return creditService.getCreditDrafts() .subscribeOn(Schedulers.io()) .observeOn(Schedulers.computation()) // map from raw to safe .flatMapObservable(Observable::fromIterable) .map(creditDraftMapper) .toList() // put mapped objects in store .doOnSuccess(store::replaceAll) .toCompletable(); } Java isn’t built for Mobile and Web in Fintech? We would like to disagree. A bank, whether it runs on servers or in the cloud, must be robust and able to work consistently for many years without a need to tweek this and that. Here it’s the ideal tool with a large, mature ecosystem and non-breaking changes. Ever popular It is by far the most used technology in Enterprise and backend. The majority of ranking sites tend to put it at the top. TIOBE, specialized in assessing and tracking the quality of software, named Java the second most popular language of 2020 and one of the top two most in demand technologies over the last 20 years! During this period, it has been only surpassed by C a couple of times. For 2020, PYPL, relying on Google Trends data, listed Java as the second most popular programming language behind Python. And here we’d like to point out that it only lost the lead in 2018. Also, JetBrains’ fourth annual developer survey placed it second, just behind JavaScript. Verdict Java is facing stiff competition from Python, often synonymous with data science and machine learning, and JS. It will probably never have the same market share it once had in the industry. However, the developers still view learning this technology as a secure investment for the future, since the number of job openings demanding these skills in Fintech is as immense as ever. And considering the amount of adoption and many recent innovations, it will remain the number one choice for financial business going forward. 2. Kotlin In recent times, Java received much criticism regarding developer experience and ergonomics: It is verbose, produces boilerplate, prone to accidental features, and lacks many modern features. We strongly believe Kotlin is the best among technologies that tried to address those shortcomings. JetBrains, the developer behind IntelliJ IDE, released Kotlin in 2016. It tried to solve the described issues without compromising the compile time. Kotlin has introduced many excellent features from other state-of-the-art and highly productive technologies. It is considered a much simpler, modern, concise alternative. Android and many Enterprise apps run on LTS versions of JDK. In such use cases, Kotlin provides the developers more powerful modern features as Kotlin can target a wide range of older JDK versions (such as 6 or 7). Kotlin received a significant boost in 2019 when Google declared it as the preferred Android and web app development option. But why is it so fast growing? Kotlin is statically typed and supports multi-paradigm (object-oriented, functional). It has introduced many modern features in JVM like Type Inference, Null Safety, Data Classes, Extensions, Coroutines. With its clean design, Kotlin resembles Python on JVM in terms of developer productivity. As the preferred language to develop for Android, it has already surpassed Java with a 60% market share in this field. The industry gave it a warm welcome. Many Fortune 500 companies are adopting Kotin, including the tech giant Google. Although Kotlin was developed mainly as a JVM language, it has recently widened its target platform support. Today it can be compiled to JS, Native Code (via LLVM). The best of both worlds Mastercard entered a partnership with Corda to develop and pilot a new blockchain-enabled cross-border payments solution.2 It connects global fast payments infrastructures, trading schemes and banks supported by a Mastercard-operated clearing and settlement network. Corda is an open source blockchain project written in Kotlin for storing, managing, and synchronizing financial obligations between various financial institutions. This example is taken from the Corda project:3 // Add this import: import net.corda.core.identity.Party // Replace TemplateState's definition with: @BelongsToContract(TemplateContract::class) class IOUState(val value: Int, val lender: Party, val borrower: Party) : ContractState { override val participants get() = listOf(lender, borrower) } If the team wanted to leverage innovation, on the one hand, and sustainability provided by the JVM, on the other, Kotlin is ideal. It attracts Fintech companies for its progressive algorithms, flat learning curve, and increased development velocity. On the rise First released in 2016, Kotlin is gaining popularity and is becoming a top trend in the near future. As of December 2020, it ranked 13th on PYPL, just behind TypeScript and Go. Seeing it above such powerhouses as Ruby (14th) is already a rather impressive result. Kotlin is designed for better developer ergonomics and enjoyed globally. It is the 4th most loved language for nearly 63% of respondents, just behind Python, as per the Stack Overflow 2020 Developer Survey. With a 182% year-over-year increase between 2019 and 2020, it is one of the fastest-growing programming technologies worldwide, according to the GitHub Octoverse. JetBrains 2020 Developer Survey stated that Kotlin is among the top three languages developers want to migrate to and adopt as their primary tool: Verdict Kotlin is one of those rare instances that strike the right balance between cool features and simplicity. It will definitely see more popularity next decade. Kotlin is an excellent choice for the curious minds who want to learn a highly productive language or test skills in modeling Android apps. Some even claim that betting on Kotlin would be a smart move for financial or banking organizations in the near future. You can read more about its features and what this technology means for the industry in BellSoft’s two-part series about Kotlin. 3. Groovy In specific scenarios, dynamically typed languages have a significant advantage over statically typed ones from the standpoint of developer speed. Inspired by Python and Ruby, James Strachen started to develop dynamically typed programming for JVM in 2003. Four years later, Groovy saw the light of day as the first of its kind. It has introduced Python-like clean coding combined with Ruby-like dynamism. Groovy is a respectable choice and widely used for the following reasons: It is optionally typed and multi-paradigm (object-oriented, functional, imperative, scripting). Groovy has brought many pragmatic features: type inference, multi-line strings, closures, prototype extensions. This approach later heavily influenced other technology, like Kotlin. It is seamlessly compatible with the JVM and boasts a colossal library ecosystem. Many important frameworks, such as Spring, feature Groovy. It can be used for Domain Specific Language (DSL) and implementing traits at runtime. Dispatching methods through the meta-object protocol instead of calling them directly makes it highly performant. Groovy is now part of the Apache foundation and is massively used in the industry. It is ideal for scripting, tooling, and DevOps for its dynamic, concise nature. Widespread tools like Gradle, Jenkins, and SoapUI use Groovy as the primary programming language. Being in DevOps and Architecture domains also extends its lifetime. Speed is key The Australian-based Auto & General Insurance Company Ltd. needed internal tooling to manage its DevOps.4 It planned to create a Maven archetype-like plugin for Gradle that generates project derivatives from local templates. For this small-scale finance project that other systems rely on, rapid development and operations are of utmost importance. Groovy is the perfect fit for interoperating with the company’s existing tools thanks to its dynamic nature, support for DSL, and huge adoption in the DevOps domain. Little goes a long way Groovy is a mature programming language offering Python-like productivity in JVM. This combination proves its advantage for a variety of smaller tasks: tooling, scripting, proof of concepts. Hence its relative popularity in finance, trading and banking regardless of not being a general-purpose language. On TIOBE, Groovy moved its position from #12 to #11 in December 2020. However, the PYPL index ranked Groovy as 23rd the same month: it seems that 0.45% market share is not something to boast about. Verdict Optionally typed Groovy is not for large-scale production in Fintech: its benefits lie in fast delivery and dynamism. Method caching and other pragmatic features are the secret behind the neat performance. Financial enterprises consider Groove a well-established tool that will have its share of users no matter what. 4. Scala The JVM veteran Martin Odersky decided to work on its drawbacks. In 2004, he released Scala that combined many functional programming features with the object-oriented paradigm. It became one of the earliest languages targeting the JVM as a runtime platform. Scala has successfully accelerated technological advancements and directly contributed to the recent modernization of Java. The industry would not be the same without this language: it has played a critical role in popularizing the functional programming paradigms in the last decade. What are its features valuable for finance and Fintech in general? It is statically typed and has a multi-paradigm model (object-oriented, functional, concurrent, imperative). Many functional programming features have been introduced with Scala, such as currying, immutability, lazy evaluation, pattern matching, type inference from Haskell and Standard ML. With its functional programming features, immutable collections and concurrent support, it is heavily used in data-intensive applications. Some of the world’s best batch and stream processing applications (Apache Spark, Apache Kafka) are written in Scala. Although it was developed mainly as a JVM language, Scala can be compiled to other intermediate representations: JS, Native Code (via LLVM), etc. It has gone through a major overhaul in Scala 3 (Dotty), addressing various criticisms and pain points. Scala 3 is more opinionated, simpler, predictable, and stable. It’s got rid of several obsolete features and reduced compile time — to save resources and increase capital. Skyrocketing performance In 2016, PayPal implemented Scala as a part of their complex finance infrastructure to increase the amounts of transactions it could receive.5 Built on Akka (a runtime and toolkit to simplify the building of apps on the JVM) with Akka Streams and powered by Scala with Kafka, their new open source squbs platform showed incredible results. Applications served a billion+ hits a day “with as little as 8 VMs and 2 vCPU each.” This change proved to be a successful investment. Before, with a Spring/Java combination, the performance was almost one-tenth of what the team achieved. PayPal’s requirements for the tech stack were: horizontal and vertical scalability, near-real-time performance, efficient usage of resources (most likely for cloud deployments), and resiliency under high burst. Below you see a perpetual stream that starts and stops with the system. It provides a convenience trait to help write streams controlled by the lifecycle with minimal to no message losses. It provides customization hooks and killSwitch (from Akka) to be embedded into the stream. class MyStream extends PerpetualStream[Future[Int]] { def generator = Iterator.iterate(0) { p => if (p == Int.MaxValue) 0 else p + 1 } val source = Source.fromIterator(generator _) val ignoreSink = Sink.ignore[Int] override def streamGraph = RunnableGraph.fromGraph( GraphDSL.create(ignoreSink) { implicit builder => sink => import GraphDSL.implicits._ source ~> killSwitch.flow[Int] ~> Sink ClosedShape } ) } Here Scala fits nicely with its functional programming features and potential interoperability with leading streaming platforms like Apache Spark and Apache Flink. Always finds its purpose Even though Scala was released with a lot of optimism, it has remained a special-purpose language. We see its adoption within Fintech flattening in the past five years. But in its domain, data-intensive applications and stream processing, Scala edges out almost all others. Moreover, many acclaimed and prevalent frameworks and runtimes support this piece of tech (e.g., Akka, Play, Finagle, Lagom), proving its strong foothold in the industry. Verdict Scala had a say in improving Java and influenced other modern tech, including Kotlin; but it has not gained broader acceptance for financial institutions. Its many breaking changes that had been introduced over the years were not well received in Fintech or banking. The one thing it’s really good at is stream processing, which is shown by the PayPal use case above. Scala 3, the latest release, finally adapts pragmatic features to become a more mainstream language. We do hope this version has the potential to make it general-purpose and used on a wider scale in finance and trading. 5. Clojure Lisp was one of the first ever high-level languages, released in 1958. Much later Rich Hickey, a noted engineer, worked to create a dynamic and functional dialect of Common Lisp to target JVM, which became Clojure. Unlike Scala that combines the object-oriented and functional paradigms, Clojure is a pure functional language. Clojure encourages immutability and immutable data structures, as well as being explicit about managing identity and its states. We may summarize its main characteristics as follows: It is dynamic, agent-oriented, and functional. By default, it has an immutable data structure and provides tail recursion. As a Lisp variant, Clojure also supports Code as Data, i.e., it can be modified automatically at runtime with Lisp macros, and treats programs as models. Like Scala and Kotlin, Clojure also can be compiled to other intermediate representations, such as JS and .NET. ClojureScript has influenced new functional languages targeting JS (e.g., Elm). With its immutable core data structure, software transactional memory (STM), and Atoms (shared, synchronous, independent state), Clojure excels at concurrent and parallel programming. Micro level analysis Nubank, the largest Fintech in Latin America that became a unicorn startup in 2018, has developed a Double Entry Accounting system with Clojure.6 The bank claims it to be “an ancient technology used for centuries” which wonderfully connects with this functional programming language. Among its requirements, Nubank mentioned that the language should be capable of parallel processing, ensure maintainability, and manage invariables. Here’s a snippet that showcases declarative rules for banking movements. (def new-purchase [{:entry/debit-account :book-account.asset/settled-brazil :entry/credit-account :book-account.liability/payable-brazil :entry/amount (comp :amount :purchase) :entry/post-date (comp time->date :time :purchase)} {:entry/debit-account :book-account.liability/payable-brazil :entry/credit-account :book-account.profit-and-loss/interchange-brazil :entry/amount (comp :interchange :purchase) :entry/post-date (comp time->date :time :purchase)} {:entry/debit-account :book-account.liability/current-limit-counterparty :entry/credit-account :book-account.asset/current-limit :entry/amount (comp :interchange :purchase) :entry/post-date (comp time->date :time :purchase)]) This allows proceeding analysis of a specific user or groups of users by any criteria behavior. For instance, the Fintech can predict capital flows or how much a customer will spend next year based on the current prices. Perfect for greenfield Clojure is getting popular with companies in finance, retail, banking, and Fintech working on large-scale projects from scratch thanks to its speed of development and clean code. It is excellent for specific use cases (e.g., concurrent programming, big data projects); slowly but steadily, it’s gaining traction in the financial industry. Still, Clojure is not yet a general-purpose, mainstream programming language. Verdict Although Clojure is excellent at what it does, it lacks the pragmatism of Kotlin or Groovy. A universal language for Enterprise needs both object-oriented and functional paradigms. We predict that Clojure will not go beyond its scope and remain special-purpose. Summary JVM is, by far, the most widely used process virtual machine in finance. It is powerful, battle-hardened, and it has passed the test of time. In this article, we have explained how financial, banking, investment, and overall Fintech companies can benefit from JVM’s versatility and summarized the top 5 JVM languages, each with its strengths and particular use cases. Any business task is possible with one of these five options. You can rest assured that in any case and resources available, your solution will be performant, implement fast, multiply your capital, and stay relevant for many years to come. If you would like to learn more about how JVM-based solutions can advance your business, contact BellSoft. The company’s engineers are willing to share best practices collected over the decade of working in the JDK world. They’ll be happy to answer all your questions and provide the Progressive Java Runtime that will satisfy your needs. Get free advice References n26/N26AndroidSamples · GitHub Mastercard and R3 Partner to Develop New Blockchain-Powered Cross-Border Payments Solution Corda · GitHub Auto & General Insurance Company Ltd. · GitHub PayPal Blows Past 1 Billion Transactions Per Day Using Just 8 VMs With Akka, Scala, Kafka and Akka Streams Building a powerful Double Entry Accounting system - Lucas Cavalcanti - [Secrets behind tiny Docker containers for Java microservices](https://bell-sw.com/announcements/2021/01/13/Secrets-behind-tiny-Docker-containers-for-Java-microservices/): In view of the upcoming Liberica JDK release, we want to lift the curtain just a bit and talk about what makes BellSoft images so small. You will learn the two main image reduction methods and get tools to minimize containers for your project. Table of Contents Introduction How to reduce container size 1. Switch to another OS image 2. Trim down JDK Conclusion Introduction Every organization knows that speed is the key. This truth applies fully to your software; the quicker it transfers and deploys, the more you will process and gain. Hence the trend for tiny Docker container images. Fast and productive, they save precious time — which equals revenue — in all environments: development, staging, and production. Building an application, we imagine a classic container consisting of multiple layers: the app itself, a framework, libraries, various packages. If you’d like to look into it more, head over to our previous article on how code becomes a microservice. Besides this structure, it delineates different technologies used in creating this architecture and the ways they interweave with one another. But when it comes to optimization (e.g., for the microservice architecture), it’s easier to see a Linux container as a tale of two parts: base image + JDK. These are the components that always affect the footprint. Now let us see which changes we’ve made to these components to shrink containers more than ever. How to reduce container size 1. Switch to another OS image The first possible answer is rather simple. Instead of a heavyweight full version of CentOS, take CentOS slim. Still not enough? Switch to Debian. Or consider the optimal choice: Alpine musl. We at BellSoft advocate for Alpine Linux as an auspicious operating system for development since its base image is the lightest among the Linux family. Take a look at these charts below. Here we compare the sizes of Liberica JDK containers (versions 11.0.10 + 11.0.10 Early Access builds) with three Linux distributions. As you can see, the Liberica EA binaries released on Jan 19 are smaller by 3–6 MB, which amounts to 14.7% for the Alpine musl java.base image. The average improvement is 7.6%. If there’s no need to compile applications inside of a Docker image, you may use either JRE or java.base. We observe similar positive changes for Liberica JRE EA: on average, these packages have experienced an impressive 16% reduction. BellSoft currently offers three flavors of Liberica JDK: Full, complete with LibericaFX (based on OpenJFX), MinimalVM, and DIO APIs, is best suited for large-scale software development. Standard is the optimal build for most desktops/servers. Lite is a lightweight Liberica flavor, making it the perfect fit for cloud instances and minimizing resources. The best part is that BellSoft Liberica Lite is not some kind of a lackluster version that will limit your project. It is a full-fledged TCK-verified runtime 100% compatible with Java SE specifications. Judging from the charts, it’s self-evident that this is the flavor we prefer in our minuscule Docker containers, for both high performance and ultimate speed. In case you are wondering whether reduced static footprint influences functionality, the answer is it does not. The binaries with reduced static footprint remain compatible with the Java SE Specification and provide all the JVM functionality that is found in the standard binaries, including all JIT compilers (C1, C2, Graal JIT Compiler), Garbage Collectors (Serial, Parallel, CMS, G1, Shenandoah, ZGC) and serviceability features, where applicable. Each release undergoes thorough QC. As part of this process, we have evaluated the binaries with major industry-standard benchmarks and measured performance with Java Microbenchmark Harness tests. Our intent was to prove no differences in functioning or slower startup times for the Lite binaries relative to Liberica Standard. A subset of the performance results for 11.0.10 EA is as follows. SpecJBB 2015 Setup JDK 11.0.10 4 repeats, average data presented Cascade Lake Intel CPU, 48 cores, 128 Gb RAM Options for backend: -server -XX:+PrintFlagsFinal -XX:+UseParallelGC -Xnoclassgc -XX:-UseAdaptiveSizePolicy -XX:+AlwaysPreTouch -XX:+UseBiasedLocking -XX:-UsePerfData -XX:-UseNUMA -XX:-UseNUMAInterleaving -XX:InlineSmallCode=20k -XX:CompileThreshold=1000 -XX:-SegmentedCodeCache -XX:MaxInlineLevel=15 -XX:ParallelGCThreads=56 -Xmx102400m -Xms102400m -Xmn76800m -XX:SurvivorRatio=130 -XX:TargetSurvivorRatio=66 -XX:MaxTenuringThreshold=15 System options: Page Size = 512 Mb DaCapo Benchmark Suite Setup JDK 11.0.10 9.12-bach-MR1 benchmark revision 20 repeats, standard deviation computed from there. Skylake Intel CPU, 4 cores Besides testing against subbenchmarks, we have calculated the geometric mean, which you can also see in the chart below. As can be seen from these graphs, no statistically significant differences with Standard have been found. We have also extensively tested with other benchmarks and have not found any benchmarks where the new lite version is worse performance-wise with statistical significance. The conducted tests lead us to conclude that Liberica Lite EA binaries v. 11.0.10 are as performant and fast to launch as Standard ones. You may download the latest packages right now or see what BellSoft has in store. 2. Trim down JDK Another option is to minimize the runtime by excluding modules you don’t need with jdeps and jlink. We’ll show you how it’s done with an example. The code inside this file is a small app you may work with to follow the steps. First, launch the Java Dependency Analysis Tool (JDeps). It processes Java bytecode, i.e., class files or the JARs that contain them, and examines the statically declared dependencies between classes. But we also see another way of using it. Being fully aware of the module system, JDeps compiles a list of JDK modules your Java application depends on. Leave only those in your native image and get rid of the rest. jdeps duke_ascii.jar duke_ascii.jar -> java.base bellsoft.duke -> java.io java.base bellsoft.duke -> java.lang java.base Having analyzed the code with jdeps, we see that it only uses java.base. In the case of a bigger app with more dependencies and libraries, we’d use jlink to reduce the runtime. Luckily, BellSoft already has a Docker image with java.base. Here’s the pre-built image with this application on DockerHub. Let’s run our app with: docker run --rm bellsoft/liberica-openjdk-demos-asciiduke. &&& &&&&&&& &&&&&&&&& &&&&&&&&&&& &&&&&&&&&&&&&&& &&&&&&&&&&&&&&&&& &&&&% %%%%%%&&&& &&&&%%%%%%%%%%%%%&&&& && &&%%%%%%%%%%%%&& &&& &&& &&%%%%%%%&&& &&&&& && && & && & & & && &&&&& &&&&& &&&& &&&& &&&&& &&&&&& & &&&&&&&&& & & && &&& && && For most similar CLI-like applications, a single java.base module will suffice. The whole sample image with Liberica JDK Lite and Alpine Linux musl is going to be as little as 40.4 MB with the app itself! Packed and run, this tiny image gives us the smallest container there is. You can also tune the JDK for resource constrained containers: refer to a dedicated guide for more detail. Conclusion Lots of developers build Docker image files and containers on their own because they are dissatisfied with what the market offers. Our instructions and lightweight Liberica JRE/JDK images got you covered so that you focus energy on your project instead. BellSoft is full of ambition and is planning further work to cut down images. Liberica’s 11.0.10 Early Access binaries show an 8 to 16 percent reduction in size over regular ones. If you would like to be among the first to know about new advanced features, contact BellSoft engineers. During the free consultation, our experts with 15+ years of Java™ experience will explain how to get EA to releases that will revolutionize your projects and optimize your solutions — whether they are desktop, server, or cloud-based. Request licensing New lightweight Linux with LTS support Alpine Linux is great for trimming down container image size, but it is based on musl libc, whose performance may be inferior to glibc in some cases. In addition, the migration from glibc to musl is anything but easy. Inspired by Alpine project, BellSoft engineers created Alpaquita Linux. It is a lightweight Linux distro based on Alpine, but with a few upgrades, such as Performance and security enhancements Optimized musl, which is equal or superior to glibc in terms of performance Additional glibc-based variant LTS releases for companies requiring enterprise support Download Apaquita and try it out! Download Alpaquita Linux for free - [Liberica JDK 8u282, 11.0.10, and 15.0.2 are here](https://bell-sw.com/announcements/2021/01/19/Liberica-8u282-11.0.10-and-15.0.2-are-here/): January marks the beginning of the year and a new JDK release. Welcome versions 8u282, 11.0.10, and 15.0.2! This time, the release contains 401 fixes overall (139 in JDK 8, 200 in JDK 11, and 62 in JDK 15). It will aid Java developers with the following features, selected and crafted by BellSoft based on the latest IT trends. 1. Support for Apple Silicon Liberica JDK now runs natively on M1-powered Apple products. This feature applies to both LTS’s (8, 11) and the current Liberica JDK 15. Given that in July 2020 we added AArch64 support for LibericaFX to JDK 11, the most recent LTS version, it paved the way to include it for 64-bit ARM processors in Macs and Macbooks. Apple Silicon users will benefit from full-fledged JavaFX (including Graphics, Controls, Media, and Webkit modules) in all Full bundles to create complex and appealing visual interfaces. When it comes to JVM features, all GCs, C1 and C2 JIT Compilers are supported. Graal JIT Compiler, Ahead-of-Time Compilation, and Class Data Sharing are explicitly not supported on this platform in this release. 2. JDK EA trimmed down by 5 MB Our last article discussed how we build minuscule Liberica JDK and JRE images suitable for cloud environments and microservices. Along with today’s new version, BellSoft releases special Early Access binaries that are even smaller than before. All are reduced by 3–6 MB in size, or 5 to 14.7 percent. Such an improvement will bring extra efficiency and savings to software development with Docker containers. The EA lite packages are available from here: JDK 11 Lite Download Links Name Size (MB) SHA1 JDK 11.0.10+10 EA Linux x86 Lite DEB 66.8 1407bf71325383afd3ccda8ee30e79454c943358 JDK 11.0.10+10 EA Linux x86 Lite RPM 68 ade756f0e0e4e9267e8bee7033f5565ef2f53df7 JDK 11.0.10+10 EA Linux x86 Lite TAR.GZ 70.8 f0135fc028a7085681d3aadf23b0a9c79169e9de JDK 11.0.10+10 EA Linux ARM Lite DEB 66 8182fd5d7878528e73d37e62e1d2478d56bb97b7 JDK 11.0.10+10 EA Linux ARM Lite RPM 67.4 d1fbca79c317a4a0789d2997e7ec2fc7f7ad391f JDK 11.0.10+10 EA Linux ARM Lite TAR.GZ 70.4 60f01b991ce6a2c63f354763ffba11a95e387eb9 JDK 11.0.10+10 EA Alpine Linux ARM (musl) Lite APK 69.1 4cb9aa8cccfba4f55a70c5f2fb33a625ac244d51 JDK 11.0.10+10 EA Linux ARM (musl) Lite TAR.GZ 70 c00dd67f9c409254b35b9f2abcf7b19ebb785464 JDK 11.0.10+10 EA Alpine Linux x86 (musl) Lite APK 69.5 86adea0b0a959da738ba207c5f5142b8cecec2a1 JDK 11.0.10+10 EA Linux x86 (musl) Lite TAR.GZ 70.5 2dc43d1d14406d3a422ff72f23512a11e9c0f897 JDK 15 Lite Download Links Name Size (MB) SHA1 JDK 15.0.2+11 EA Linux x86 Lite DEB 67.4 27fba8134bb6100473057b34fcbb4fd8e667bce8 JDK 15.0.2+11 EA Linux x86 Lite RPM 68.7 fc618887c6c7c6ea42cce9b9b07ad787a979af4e JDK 15.0.2+11 EA Linux x86 Lite TAR.GZ 71.1 c62a776af52b69a525df6765066563a3008b5a4c JDK 15.0.2+11 EA Alpine Linux x86 (musl) Lite APK 70.1 419aaab372e599f53917bba4021c7a6660b7baa3 JDK 15.0.2+11 EA Linux x86 (musl) Lite TAR.GZ 71 2d6fd2a8f73cbc33c9a2208d7ba50b8f3d511df3 JDK 15.0.2+11 EA Linux ARM Lite DEB 66.8 97ce0e29bdcca8b26243cc3c56bd00b5b19e308a JDK 15.0.2+11 EA Linux ARM Lite RPM 68.2 1e2f6f06e99a4fad8df118f806cd88d86515ebd9 JDK 15.0.2+11 EA Linux ARM Lite TAR.GZ 70.8 a63ec89c90d1e23c9d4f39af7726965c29b78856 JDK 15.0.2+11 EA Alpine Linux ARM (musl) Lite APK 69.9 d7b9c8114f7ef011f786cccab761219075f72631 JDK 15.0.2+11 EA Linux ARM (musl) Lite TAR.GZ 70.8 c45f55ac62727c3686e5cc9f06698beba804b2ca The EA JDK packages that can be used to build arbitrary runtimes with jlink are available from here: JDK 11 Download Links Name Size (MB) SHA1 JDK 11.0.10+10 EA Linux x86 DEB 156.1 39fa6a91d82bbe400231a08bd803a9356bd49a0e JDK 11.0.10+10 EA Linux x86 RPM 162.7 584418f982acdf769490fbef0b331d8e82943077 JDK 11.0.10+10 EA Linux x86 TAR.GZ 181.3 fe9ea1d8205ce2f97fc9ecef250e368c04bd3e88 JDK 11.0.10+10 EA Linux ARM DEB 163 6975ad025cbd45abbfadffdf148bab1274071113 JDK 11.0.10+10 EA Linux ARM RPM 170 435fa09411e6440015783dae5f0e46910ff1ce20 JDK 11.0.10+10 EA Linux ARM TAR.GZ 189.7 884895c6119ee42e22b46ee19365bcf9d7bed6f9 JDK 11.0.10+10 EA Alpine Linux ARM (musl) APK 178.7 7fed224f6e7146a9c7144c63c2e387457fc4c968 JDK 11.0.10+10 EA Linux ARM (musl) TAR.GZ 179.8 5516161846ef020525eb7ba29384fecce3438594 JDK 11.0.10+10 EA Alpine Linux x86 (musl) APK 179.5 67dc4c1ca1460bef475dd279c4bb3617c1777010 JDK 11.0.10+10 EA Linux x86 (musl) TAR.GZ 180.7 3d648000304deb723e7df47ccb7534454663a0e1 JDK 15 Download Links Name Size (MB) SHA1 JDK 15.0.2+11 EA Linux x86 DEB 162.3 1461554cb7e14032137f6b8d85c9c6c491c02b9e JDK 15.0.2+11 EA Linux x86 RPM 169.2 05ff5748741b8d448f8e9ef56730dc1c9a84f834 JDK 15.0.2+11 EA Linux x86 TAR.GZ 188.1 f0a140f12be70617fa5a23a479ac8b132680a4a5 JDK 15.0.2+11 EA Alpine Linux x86 (musl) APK 186.7 178f41ec9ae015192a0f9b48175913c77de45a49 JDK 15.0.2+11 EA Linux x86 (musl) TAR.GZ 187.9 49708830569af388a934e12fff1cd8dbf98d992c JDK 15.0.2+11 EA Linux ARM DEB 164.3 1461554cb7e14032137f6b8d85c9c6c491c02b9e JDK 15.0.2+11 EA Linux ARM RPM 170.9 05ff5748741b8d448f8e9ef56730dc1c9a84f834 JDK 15.0.2+11 EA Linux ARM TAR.GZ 189.5 f0a140f12be70617fa5a23a479ac8b132680a4a5 JDK 15.0.2+11 EA Alpine Linux ARM (musl) APK 186.3 fa09b54fee8063719fe40da1b17c0767e02021f9 JDK 15.0.2+11 EA Linux ARM (musl) TAR.GZ 187.6 77730c58610e8bb8168c0456738bbf50c3f3fd79 Here’s how to build a Docker image with these files: JDK 11 Alpine-musl Base 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM alpine:3.11 ENV LIBERICA_VERSION=11.0.10+10-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-x64-musl.tar.gz ENV LIBERICA_SHA=e621f4115f6d357b31b1b8888c42329c2c78d575e9e24ddbe8c2162a8caf5874 ENV LIBERICA_HOME=/opt/bellsoft/jdk SHELL ["/bin/ash", "-o", "pipefail", "-c"] RUN set -o errexit -o nounset \ && apk add --no-cache binutils \ && wget --no-verbose --output-document=jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p /tmp/jdk \ && tar xf jdk.tar.gz -C /tmp/jdk --strip-components=1 \ && rm jdk.tar.gz \ && /tmp/jdk/bin/jlink \ --add-modules java.base \ --compress=2 \ --no-header-files \ --no-man-pages \ --module-path /tmp/jdk/jmods \ --output "$LIBERICA_HOME" \ && rm -rf /tmp/jdk \ && apk del binutils \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run --rm name:tag JDK 11 Alpine-musl 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM alpine:3.11 ENV LIBERICA_VERSION=11.0.10+10-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-x64-musl-lite.tar.gz ENV LIBERICA_SHA=cf561299f4e3540ee5bc706a1c1bffb2d8156a67413e5489bd818fdd7c2356ce ENV LIBERICA_HOME=/opt/bellsoft/jdk SHELL ["/bin/ash", "-o", "pipefail", "-c"] RUN set -o errexit -o nounset \ && wget --no-verbose --output-document=jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p "$LIBERICA_HOME" \ && tar xf jdk.tar.gz -C "$LIBERICA_HOME" --strip-components=1 \ && rm jdk.tar.gz \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run --rm name:tag JDK 11 Debian 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM debian:9 ENV LIBERICA_VERSION=11.0.10+10-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-amd64-lite.tar.gz ENV LIBERICA_SHA=4f362dd78b00cc077d770dfa5331cb0ae7d6ed124f8a73dea560a1dee0793606 ENV LIBERICA_HOME=/opt/bellsoft/jdk RUN set -o errexit -o nounset \ && apt-get update \ && apt-get install --yes --no-install-recommends \ ca-certificates \ curl \ fontconfig \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* RUN set -o errexit -o nounset \ && curl --fail --location --silent --show-error --output jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p "$LIBERICA_HOME" \ && tar xf jdk.tar.gz -C "$LIBERICA_HOME" --strip-components=1 \ && rm jdk.tar.gz \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run –rm name:tag JDK 11 CentOS 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM centos:7 ENV LIBERICA_VERSION=11.0.10+10-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-amd64-lite.tar.gz ENV LIBERICA_SHA=4f362dd78b00cc077d770dfa5331cb0ae7d6ed124f8a73dea560a1dee0793606 ENV LIBERICA_HOME=/opt/bellsoft/jdk RUN set -o errexit -o nounset \ && yum install --assumeyes \ ca-certificates \ curl \ fontconfig \ && yum clean all \ && rm -rf /var/cache/yum \ && rm -rf /var/lib/rpm/__db* \ && rpm --rebuilddb RUN set -o errexit -o nounset \ && curl --fail --location --silent --show-error --output jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p "$LIBERICA_HOME" \ && tar xf jdk.tar.gz -C "$LIBERICA_HOME" --strip-components=1 \ && rm jdk.tar.gz \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run –rm name:tag JDK 15 Alpine-musl Base 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM alpine:3.11 ENV LIBERICA_VERSION=15.0.2+11-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-x64-musl.tar.gz ENV LIBERICA_SHA=a7e1ef6d00b16ff61d982fef94584b73533e3732b8b0c92355fdb08867758cba ENV LIBERICA_HOME=/opt/bellsoft/jdk SHELL ["/bin/ash", "-o", "pipefail", "-c"] RUN set -o errexit -o nounset \ && apk add --no-cache binutils \ && wget --no-verbose --output-document=jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p /tmp/jdk \ && tar xf jdk.tar.gz -C /tmp/jdk --strip-components=1 \ && rm jdk.tar.gz \ && /tmp/jdk/bin/jlink \ --add-modules java.base \ --compress=2 \ --no-header-files \ --no-man-pages \ --module-path /tmp/jdk/jmods \ --output "$LIBERICA_HOME" \ && rm -rf /tmp/jdk \ && apk del binutils \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run –rm name:tag JDK 15 Alpine-musl 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM alpine:3.11 ENV LIBERICA_VERSION=15.0.2+11-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-x64-musl-lite.tar.gz ENV LIBERICA_SHA=67e4f6d85fcd28d25d62d314e4a8bb8aa536b843e6476045c5586b5e85038eb0 ENV LIBERICA_HOME=/opt/bellsoft/jdk SHELL ["/bin/ash", "-o", "pipefail", "-c"] RUN set -o errexit -o nounset \ && wget --no-verbose --output-document=jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p "$LIBERICA_HOME" \ && tar xf jdk.tar.gz -C "$LIBERICA_HOME" --strip-components=1 \ && rm jdk.tar.gz \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run –rm name:tag JDK 15 Debian 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM debian:9 ENV LIBERICA_VERSION=15.0.2+11-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-amd64-lite.tar.gz ENV LIBERICA_SHA=7e490719b0fc08cf68b9524e14833e3f53aa378c7e94e17ca93a6bc6317b0315 ENV LIBERICA_HOME=/opt/bellsoft/jdk RUN set -o errexit -o nounset \ && apt-get update \ && apt-get install --yes --no-install-recommends \ ca-certificates \ curl \ fontconfig \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* RUN set -o errexit -o nounset \ && curl --fail --location --silent --show-error --output jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p "$LIBERICA_HOME" \ && tar xf jdk.tar.gz -C "$LIBERICA_HOME" --strip-components=1 \ && rm jdk.tar.gz \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run –rm name:tag JDK 15 CentOS 1. Create an empty directory and cd into it. 2. Create a file named Dockerfile and insert the following code: FROM centos:7 ENV LIBERICA_VERSION=15.0.2+11-ea ENV LIBERICA_URL=https://download.bell-sw.com/java-ea/$LIBERICA_VERSION/bellsoft-jdk${LIBERICA_VERSION}-linux-amd64-lite.tar.gz ENV LIBERICA_SHA=7e490719b0fc08cf68b9524e14833e3f53aa378c7e94e17ca93a6bc6317b0315 ENV LIBERICA_HOME=/opt/bellsoft/jdk RUN set -o errexit -o nounset \ && yum install --assumeyes \ ca-certificates \ curl \ fontconfig \ && yum clean all \ && rm -rf /var/cache/yum \ && rm -rf /var/lib/rpm/__db* \ && rpm --rebuilddb RUN set -o errexit -o nounset \ && curl --fail --location --silent --show-error --output jdk.tar.gz "$LIBERICA_URL" \ && echo "$LIBERICA_SHA *jdk.tar.gz" | sha256sum -c - \ && mkdir -p "$LIBERICA_HOME" \ && tar xf jdk.tar.gz -C "$LIBERICA_HOME" --strip-components=1 \ && rm jdk.tar.gz \ && "$LIBERICA_HOME/bin/java" -version ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV JAVA_HOME=$LIBERICA_HOME ENV PATH=$LIBERICA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CMD ["java", "-version"] 3. Build: docker build . -t name:tag 4. Run image with: docker run –rm name:tag 3. New architecture/OS combination If previously Liberica only supported Alpine Linux on Intel x86, this release extends it to AArch64. We see that the ARM architecture is becoming increasingly more prevalent in software development and cannot help but deliver. 4. Liberica installers and repository for Alpine Linux (Intel & AArch64) Before, Liberica binaries for Linux distributions could only be downloaded as digitally signed TAR.GZ archives, DEB or RPM files. Now we offer a possibility to install JDK and JRE packages on Alpine Linux with a standard apk add procedure. Here’s how you do it. To add BellSoft official Alpine Linux repository, use the following commands: ## add our repository echo "https://apk.bell-sw.com/main" | sudo tee -a /etc/apk/repositories ## add our official signing key sudo wget -P /etc/apk/keys/ https://apk.bell-sw.com/info@bell-sw.com-5fea454e.rsa.pub Now you install Liberica as easily as typing the following command: sudo apk add bellsoft-java8 It’s also possible to fetch the package directly from BellSoft’s website with busybox wget. Find the desired package on the Downloads page. Copy its URL and paste it in the command line, i.e.: wget https://download.bell-sw.com/java-ea/15.0.2+11-ea/bellsoft-jdk15.0.2+11-ea-linux-aarch64-musl-lite.apk Then install the newly downloaded package locally: sudo apk add --allow-untrusted bellsoft-jdk15.0.2+11-ea-linux-aarch64-musl-lite.apk You can omit the --allow-untrusted option in case if you’ve already added our official signing key as described above. We continue making our products more helpful for your day-to-day development tasks. You can rely on BellSoft to look into what the community needs and bring it to Liberica users. All builds are ready for download on our website. - [JDK Flight Recorder, The Programmatic Way](https://bell-sw.com/announcements/2021/01/29/JDK-Flight-Recorder-The-Programmatic-Way/): JDK Flight Recorder (JFR) is a powerful diagnostic tool built into OpenJDK. In previous posts, I was focusing on using JFR together with JDK Mission Control, a visual front end. Besides out of box integration with JDK tools like Mission Control and jcmd, Flight Recorder has an API. In this post, I would like to focus on the API features of JDK Flight Recorder. JDK Flight Recorder is a part of OpenJDK, so is its API. If your JDK distribution is JFR enabled, you can access it via API too; no extra dependency required. I can highlight three prominent use cases for using the Flight Recorder API. Control Flight Recorder. There are multiple ways to control JFR sessions: jcmd, JMX, JVM arguments. Handling sessions with API is an extra option offering you even more flexibility. Read events from JFR binary files. This can be handy if you want to build automated reporting or integrate JFR with your monitoring stack. Custom processing of recordings is also essential for the next use case - custom events. Create application-specific Flight Recorder events. You can leverage JFR infrastructure to produce events specific to your applications. Features such as periodic events and thresholds are also available for custom events. These JFR API use cases are rather orthogonal. You may be interested only in event generation or parsing JFR files. Introducing simple event JDK Flight Recorder is designed around events. A JFR event is a simple data record, and its type defines fields available in the data record. Fields can be of simple types such as string, numbers, or boolean. There is also special handling for classes and threads, but other complex objects are not supported. Finally, a stacktrace could be associated with each event. JVM has more than a hundred built-in event types. Besides that, your application can define new custom event types and start producing related events, which will later be stored in JFR recording files and available through JFR capable tools such as Mission Control. Let’s start instrumenting a simple Spring Web application. First, we will be generating an event for each HTTP call. You would, of course, require OpenJDK with JDK Flight Recorder support. Use OpenJDK 11 or greater; in case you prefer OpenJDK 8, pick version 8u262 or newer. You may also want to have Mission Control ready. BellSoft has a suitable distribution of Liberica JDK and a Mission Control executable. To declare a new JFR event, we should first declare a subclass of the jdk.jfr.Event class. Let’s consider a very simple event with a few custom fields. package com.example.restservice; import jdk.jfr.Event; public class RestCallEvent extends Event { public String path; public String key; public long dataSize; } Now we need instrument application code to produce events. package com.example.restservice; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class DemoController { private final Map<String, String> store = new ConcurrentHashMap<>(); @GetMapping("/get") public String get( @RequestParam(value = "key") String key) { RestCallEvent event = new RestCallEvent(); event.begin(); event.key = key; try { String value = store.getOrDefault(key, "Not Found"); event.dataSize = value.length(); return value; } finally { event.end(); event.commit(); } } @GetMapping("/set") public String set( @RequestParam(value = "key") String key, @RequestParam(value = "val", defaultValue="") String val) RestCallEvent event = new RestCallEvent(); event.begin(); event.key = key; try { store.put(key, val); event.dataSize = val.length(); return val; } finally { event.end(); event.commit(); } } } The jdk.jfr.Event base class automatically adds timestamp and event duration fields, though we need to demarcate the event’s duration using being() and end() methods. The commit() method tells JFR that event data is complete and could be put into the event buffer. In our example, each event has a new instance of an event object created for it, which is not necessary. We can reuse event instances for multiple events. It makes a garbage-free JFR events producer totally possible (although you would need to implement instance per thread or some other thread-safe object reuse pattern). It’s time to test our new event. While the application is up and running, you can start the Flight Recorder session with jcmd using the following command (5016 is a JVM process PID in my case): > jcmd 5016 JFR.start 5016: Started recording 1. No limit specified, using maxsize=250MB as default. Use jcmd 5016 JFR.dump name=1 filename=FILEPATH to copy recording data to file. Notice the command hint provided by jcmd to dump out the flight recording later. When recording is active, hit your HTTP endpoint a few times. Now we can dump the recorded JFR events and stop the recording session. > jcmd 5016 JFR.dump name=1 filename=myrecording.jfr 5016: Dumped recording "1", 726.3 kB written to: /home/aragozin/myrecording.jfr > jcmd 5016 JFR.stop name=1 Try opening this file in JDK Mission Control, and you’ll find our custom events in the “Event Browser” report. Let’s also try another tool from JDK’s arsenal. jfr is a little command-line tool that should be available in your OpenJDK distribution. It can parse JFR files and extract event data. The command below will print our events filtered by event type (a fully qualified name of an event class). > jfr.exe print --events "com.example.restservice.RestCallEvent" myrecording.jfr com.example.restservice.RestCallEvent { startTime = 05:38:28.704 duration = 17.781 us path = "get" key = "ABC" dataSize = 10 eventThread = "http-nio-8080-exec-8" (javaThreadId = 24) stackTrace = [ com.example.restservice.DemoController.get(String) line: 29 sun.reflect.NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) sun.reflect.NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43 java.lang.reflect.Method.invoke(Object, Object[]) line: 498 ... ] } com.example.restservice.RestCallEvent { startTime = 05:38:45.819 duration = 5.803 us path = "set" key = "ABC" dataSize = 59 eventThread = "http-nio-8080-exec-5" (javaThreadId = 21) stackTrace = [ com.example.restservice.DemoController.set(String, String) line: 47 sun.reflect.NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) sun.reflect.NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43 java.lang.reflect.Method.invoke(Object, Object[]) line: 498 ... ] } com.example.restservice.RestCallEvent { startTime = 05:39:00.382 duration = 9.246 us path = "get" key = "XYZ" dataSize = 9 eventThread = "http-nio-8080-exec-3" (javaThreadId = 19) stackTrace = [ com.example.restservice.DemoController.get(String) line: 29 sun.reflect.NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) sun.reflect.NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43 java.lang.reflect.Method.invoke(Object, Object[]) line: 498 ... ] } Here you can see the details of the events we’ve just produced. Fields defined in event class have become event attributes. There are also several attributes added to every custom event type. startTime — timestamp of the event. duration — time between start() and stop() method calls. eventThread — reference to a Java thread that has produced an event. stackTrace — stack trace at event.commit() call. Congratulations, we have our first custom JFR event recorded. It wasn’t hard at all. JFR events versus logging Good, we can produce JFR events. But how is it different from plain old logging? Custom JFR events are not a logging replacement, as you can probably benefit from both in the same application. Below are the most significant features of JFR events. Low overhead. JFR events are optimized to reduce the impact on application code performance. Logging in a heavy, loaded application can easily become a performance bottleneck and contention point. JFR infrastructure is well optimized for concurrent and latency-sensitive applications. Thresholds. You can set a duration threshold and keep only “slow” events. Periodic events. After scheduling events, JFR will take care of calling your event producer. Concurrent session management. The same JVM may have multiple Flight Recorder sessions running in parallel (e.g., one that continues on start-up and one initiated via jcmd). In this case, JVM will route your events to both recordings according to individual settings for each. Compact stack trace storage. Java stacktraces could be a killer for your logging infrastructure. JFR files are storing stacktraces in a compacted binary form much more efficiently than as plain text. On the other hand, logs are flexible, and you are free to put everything into your log files. With JFR, you should keep your event structured and compact to get the most value out of JFR infrastructure. You can store arbitrary strings in JFR events, but large events are hard to manage. Both your runtime overhead and resulting recording file sizes will grow proportionally to the amount of events’ data. JDK Flight Recorder is not a silver bullet. Still, having implemented JFR style telemetry myself, I appreciate API simplicity, runtime efficiency and powerful dynamic management features brought by JFR infrastructure free of charge. More JFR events Let’s improve our current event and add more to showcase them. First, add some metadata to our event and attributes. The @Label annotation allows putting human-readable names on events and attributes. They will be visible in Mission Control. With @Description, you can add more details. @Category is essential for filtering and navigating in Mission Control’s “Event Browser” report. @DataAmmout and a few other annotations mark measurement units for numeric attributes. package com.example.restservice; import jdk.jfr.Category; import jdk.jfr.DataAmount; import jdk.jfr.Description; import jdk.jfr.Event; import jdk.jfr.Label; @Category("MyEvents") @Label("REST Event") @Description("REST Request Processing Event") public class RestCallEvent extends Event { @Label("Request Path") public String path; @Label("Request Key") public String key; @Label("Result Size") @DataAmount(DataAmount.BYTES) public long dataSize; } Now we’ll introduce a periodic event. It would be a cache statistics event in our case. package com.example.restservice; import java.lang.ref.WeakReference; import java.util.Map; import jdk.jfr.Category; import jdk.jfr.Description; import jdk.jfr.Event; import jdk.jfr.FlightRecorder; import jdk.jfr.Label; import jdk.jfr.Period; import jdk.jfr.StackTrace; @Category("MyEvents") @Label("Cache Stats") @Description("Simple cache statistics") @Period("10s") @StackTrace(false) public class CacheStatsEvent extends Event { @Label("Cache Name") public String cacheName; @Label("Cache Size") public int cacheSize; public static void enableStatsRecording(String cacheName, Map ?> cache) { final WeakReference<Map ?>> ref = new WeakReference<Map>(cache); final CacheStatsEvent event = new CacheStatsEvent(); event.cacheName = cacheName; FlightRecorder.addPeriodicEvent(CacheStatsEvent.class, new Runnable() { @Override public void run() { Map ?> cache = ref.get(); if (cache == null) { FlightRecorder.removePeriodicEvent(this); } else { event.begin(); event.cacheSize = cache.size(); event.commit(); } } }); } } Periodic events require explicit registration of callbacks with JDK Flight Recorder. While it’s allowed to have multiple callbacks for the same event type, you also have to manage their deregistration. In the example above, I’m using a weak reference to avoid accidental memory leaks and automatically unregister callback once the GC clears the cache object. There is also a particular type of period called “everyChunk” that will instruct JFR to trigger event callback at least once per recording file. It’s useful for dumping immutable configuration data, so there is little point in writing the same data to the same JFR file more than once. “everyChunk” is the default value for the @Period annotation. Let’s introduce a cache configuration event, too. package com.example.restservice; import java.lang.ref.WeakReference; import jdk.jfr.Category; import jdk.jfr.Description; import jdk.jfr.Event; import jdk.jfr.FlightRecorder; import jdk.jfr.Label; import jdk.jfr.Period; import jdk.jfr.StackTrace; @Category("MyEvents") @Label("Demo Config") @Description("Demo Controller Configuration") @Period() @StackTrace(false) public class DemoConfigEvent extends Event { @Label("Set is enabled") public boolean isSetEnabled; @Label("Get is enabled") public boolean isGetEnabled; public static void enableConfigRecording(DemoController controller) { final WeakReference<DemoController> ref = new WeakReference<DemoController>(controller); final DemoConfigEvent event = new DemoConfigEvent(); FlightRecorder.addPeriodicEvent(DemoConfigEvent.class, new Runnable() { @Override public void run() { DemoController controller = ref.get(); if (controller == null) { FlightRecorder.removePeriodicEvent(this); } else { event.begin(); event.isGetEnabled = controller.isGetEnabled(); event.isSetEnabled = controller.isSetEnabled(); event.commit(); } } }); } } The @StackTrace annotation was used to disable stacktraces for our periodic events. The full source code of the example is available on GitHub. Configuring custom events Now we have several custom events. Default recording settings for each custom event are defined with annotations on event class declaration — a good head start. And yet, soon you’d possibly want to finetune these settings when starting a JDK Flight Recorder session. JFR session is customized via an XML configuration file. The recommended way to produce this configuration is through JDK Mission Control. Provided our application is running, you can use JDK Mission Control to connect to it and open the “Start Flight Recording” dialog. There, hit the “Template manager” button: you will get a list of configurations. Copy some existing configuration and open the “Edit” dialog on that copy. Now switch to advanced mode and click “Refresh from server.” The advanced mode in the template editor allows modifying settings for every event known to JDK Flight Recorder. A list of events and their metadata are available at running. “Refresh from server” pulls these data from the connected JVM, and you can see our custom events in the tree. Now you have a choice: modify this configuration and use it with JDK Mission Control, or export it to a file to control JDK Flight Recorder from a console with the jcmd tool. Controlling Flight Recorder via API There are multiple ways to start flight recording, and one of them is an API. You can programmatically prepare a configuration of the Flight Recorder session and start it or schedule the start later. There’s a possibility to dump recorded events at any time and/or stop your recording session. Why would anybody want to do it via API if jcmd and Mission Control can do it? For multiple reasons. One is to remove extra options from JVM start command and manage an automatic Flight Recording session start with application configuration. Another could be to control JDK Flight Recorder via HTTP instead of JMX. My personal favorite case is automatic crash dumps. The idea of an automatic crash dump is to produce exhaustive diagnostic information, but only if the application hits a critical fault condition. An automatic heap dump on particular exceptions illustrates this pattern nicely. How does JDK Flight Recorder fit the pattern? We can configure a background Flight Recorder session with verbose event level, but not persisted on disk (to avoid IO overhead). Events would be buffered in a fixed size memory buffer by Flight Recorder, then evicted to make room for new events. If a critical condition was encountered, we would dump recently collected events to the crash dump file. Below is a simple example of a helper class for this pattern. package com.example.restservice; import java.io.IOException; import java.nio.file.Path; import java.text.ParseException; import jdk.jfr.Configuration; import jdk.jfr.EventType; import jdk.jfr.FlightRecorder; import jdk.jfr.Recording; public class CrashDumpHelper { private static Recording rec; public static synchronized void activate() throws IOException, ParseException { if (rec != null) { Configuration conf = Configuration.getConfiguration("default"); rec = new Recording(conf); configureEvents(rec); // disable disk writes rec.setToDisk(false); rec.start(); } } private static void configureEvents(Recording rec) { for (EventType et: FlightRecorder.getFlightRecorder().getEventTypes()) { if (isEnabledForCrachDump(et)) { rec.enable(et.getName()); } } } private static boolean isEnabledForCrachDump(EventType et) { // enabled application specific custom events ... } public void dump(Path filename) throws IOException { rec.dump(filename); } } A caveat here is that such a Flight Recorder dump is not very useful without intensive instrumentation of your code with custom events. You need application-specific instrumentation to be able to reconstruct a sequence of events leading to failure. In such a case, the low overhead of JFR events is a great advantage because your intensive instrumentation needs to have a low impact on the application’s runtime performance. Reading JFR events from file The jfr tool from the OpenJDK toolset has the option to convert a JFR file to JSON. Combined with JSON processing tools such as jq, this could be enough to build sophisticated automated reporting out of JFR files. However, if Java itself is your favorite tool for data processing, reading data out of JFR files directly is even easier. The JFR file parser is a part of JVM runtime alongside other parts of JDK Flight Recorder. You can use the jdk.jfr.consumer.RecordingFile class to open a JFR file from disk. RecordingFile allows iterating through individual events in the file. Below is a code example parsing the JFR dump file and printing out one of our custom events introduced earlier in the post. package com.example.restservice; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAmount; import java.time.temporal.TemporalUnit; import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.consumer.RecordingFile; public class EventPrinter { private static final String REST_CALL_EVENT = RestCallEvent.class.getName(); public static void main(String[] args) throws IOException { String recFile = args[0]; RecordingFile rec = new RecordingFile(Paths.get(recFile)); while (rec.hasMoreEvents()) { RecordedEvent event = rec.readEvent(); if (REST_CALL_EVENT.equals(event.getEventType().getName())) { Instant startTime = event.getStartTime(); String path = event.getString("path"); String key = event.getString("key"); long size = event.getLong("dataSize"); long duration = event.getDuration().get(ChronoUnit.NANOS); System.out.println(String.format( "%s start=%s path=%s key=%s dataSize=%d duration=%dns", REST_CALL_EVENT, startTime, path, key, size, duration)); } } rec.close(); } } JFR event streaming in OpenJDK 14 Starting with JDK 14 it is also possible to access events produced by JDK Flight Recorder at real-time. New API allows users to react to events immediately as they are produced, which opens new ways for integration with monitoring infrastructure. The same API also allows “tailing” on disk JFR files and getting your code to consume events as they are being written to disk. Later is a good option to decouple your monitoring agent from the application JVM. Conclusion JDK Flight Recorder is a very powerful tool event without an API. Yet, using features such as custom events and OpenJDK 14 event streaming can be real game changers for the serviceability story of your application. JDK Flight Recorder has walked a long way from initial implementation in JRockit through a closed source feature of Oracle’s JDK to finally become truly open source and an integral part of OpenJDK (including JDK 8), and further bring up new features with OpenJDK 14. Most certainly, advanced features require integration for the application side. Also, it takes time for the Java ecosystem to catch up and widely adopt JFR as the default telemetry infrastructure at the libraries and frameworks level. I’m positive this adoption is coming. Related Posts JDK Flight Recorder – a gem hidden in OpenJDK Hunting down code hotspots with JDK Flight Recorder Hunting down memory issues with JDK Flight Recorder JVM in Linux containers, surviving the isolation - [TeXnical Writing Part 3: Syntax](https://bell-sw.com/announcements/2021/02/05/TeXnical-Writing-Part-3-Syntax/): Welcome to the third part of developing a Liberica JDK-based application for real-time conversion of mathematical formulas from Markdown to HTML. In the previous part we developed a plain Markdown editor and preview panel; in this part we’ll walk through adding a TeX processor to the application. Introduction Syntax highlighting in text editors has a colourful history, owing its emergence to the first syntax-directed editor in 1969. Sixteen years elapsed before colour syntax was introduced, an invention to make BASIC programming easier for beginners, especially children. One way of adding colour to text is to parse the file contents using regular expressions. Another way is to build an abstract syntax tree (AST). Given that our mdtexfx program includes a library that builds a Markdown AST, we’ll reuse that functionality to stylize the document. We’ll encounter the visitor pattern along the way, which helps perform arbitrary actions on items within a hierarchical collection. This pattern lets us decouple the abstract syntax tree from the code that performs the highlighting, in line with the open/closed principle. To implement syntax highlighting, we’ll first need to import a text editor that can be styled. Once that editor is in place, we’ll employ the visitor pattern to change the document’s font styles and colours. Gradle A rich text editor is a graphical user interface widget that provides developers with the ability to add style to otherwise plain text content, including: bold, italics, and colour. The StyleClassedTextArea class, which is a rich text editor developed for JavaFX, provides us with fine control over how text is presented within our editor. Keep in mind that JavaFX comes bundled with the full version of Liberica JDK. Update the build.gradle script dependencies section to include the RichTextFX library as follows: implementation 'org.fxmisc.richtext:richtextfx:0.10.5' The new text area is now available, so let’s update our application to use it. App Replacing the existing text area with one that can be styled will entail a few changes to the App class, including: Preview — Revise the HtmlPreview panel to resize dynamically, giving users the ability to reconfigure the application’s dimensions to suit their preferences. Scrollbars — Wrap the new text area with scrollbars so that users receive visual clues when content is off-screen. To adorn the application with visible scrollbars, a VirtualizedScrollPane must be constructed using an instance of StyleClassedTextArea. SplitPane — Replace the fixed BorderPane class with a SplitPane to afford users the ability to change how much screen real estate is occupied by the editor and preview components. By default, the SplitPane will evenly divide the space allotted between the two components added. Dimensions — Add default dimensions when constructing the Scene class. Update the start method of the App class to apply the changes using the following snippet: @Override public void start( final Stage stage ) { final var editor = new StyleClassedTextArea(); final var vsPane = new VirtualizedScrollPane<>( editor ); final var preview = new HtmlPreview(); final var context = new ProcessorContext( TEXT_MARKDOWN, preview ); final var processor = ProcessorFactory.create( context ); editor.textProperty().addListener( ( c, o, n ) -> processor.apply( n ) ); final var pane = new SplitPane(); pane.getItems().addAll( vsPane, preview ); final var scene = new Scene( pane, 1280, 720 ); stage.setScene( scene ); stage.show(); } We’ll come back to this later to instantiate a syntax highlighter that we’ll give to the ProcessorContext. HtmlPreview The WebView class has default dimensions of 800x600 pixels, which can be changed by setting its preferred dimensions (i.e., width and height). We’d like to resize the WebView whenever its compositional class—HtmlPreview, in this case—is resized. A few tweaks are needed to accomplish this task: HtmlPreview — Change the class such that it resizes based on its parent node’s size. WebView — Bind its preferred dimensions to the dimensions of HtmlPreview. First, change the HtmlPreview class definition to inherit from Region instead of Parent. This change ensures that the preview pane can be resized by its parent node: the SplitPane. The update looks as follows: public class HtmlPreview extends Region implements HtmlRenderer { Next, revise the HtmlPreview constructor to bind the WebView class member variable’s (mView) preferred dimensions to those of the HtmlPreview class itself, which resembles the following: public HtmlPreview() { mHtmlDocument.append( HTML_PREFIX ); mView = new WebView(); mView.prefWidthProperty().bind( widthProperty() ); mView.prefHeightProperty().bind( heightProperty() ); getChildren().add( mView ); } Run the program. Here is a screenshot that shows part of this document being edited, including a screenshot of our application within itself: Stylish Visitor Given the processor-based architecture for mdtexfx, we want to avoid polluting the generic processors with format-specific code. For example, a reStructuredText editor’s ProcessorContext must not require Markdown syntax highlighting. Instead, we’ll declare an interface that defines how any document can be highlighted, in general terms. Skeletons Before we begin coding, let’s plan our approach. Even though we don’t yet know the low-level technical details for how the code will fit together, we can jot down the high-level requirements in terms of class and interface responsibilities. The big-picture items include: SyntaxHighlighter — Defines the interface that all syntax highlighters must implement. PlaintextHighlighter — Default implementation for unsupported file formats. MarkdownHighlighter — Provides functionality that stylizes Markdown documents. HighlighterFactory — Responsible for creating a highlighter based on the document type. Create a new package named com.mdtexfx.processors.highlighters. Inside the package, create the following interface definition: package com.mdtexfx.processors.highlighters; public interface SyntaxHighlighter { } It has no working innards—hence the term skeleton—which is perfectly fine; we’ll define how it works later. We need not worry about everything all at once. Next, in the same package, create the PlaintextHighlighter and MarkdownHighlighter classes such that they both implement the SyntaxHighlighter interface: public class PlaintextHighlighter implements SyntaxHighlighter { } And: import org.fxmisc.richtext.StyleClassedTextArea; public class MarkdownHighlighter implements SyntaxHighlighter { private final StyleClassedTextArea mEditor; public MarkdownHighlighter( final StyleClassedTextArea editor ) { mEditor = editor; } } Once again, we can sort out the details of how the highlighter will do its job later. To finish up, we’ll borrow a similar approach to the ProcessorFactory for implementing the HighlighterFactory: import com.mdtexfx.io.MediaType; import org.fxmisc.richtext.StyleClassedTextArea; public class HighlighterFactory { public static SyntaxHighlighter create( final MediaType mediaType, final StyleClassedTextArea editor ) { return switch( mediaType ) { case TEXT_MARKDOWN -> new MarkdownHighlighter( editor ); case UNDEFINED -> new PlaintextHighlighter(); }; } } Notice that the creation of syntax highlighters represents different information from the creation of processors. Even though the code snippet closely resembles that of the ProcessorFactory, we have not violated the DRY principle. (See the previous article for more details about the DRY principle.) ProcessorContext With the highlighter class skeletons in place, we can now give the ProcessorContext class a syntax highlighting mechanism as follows: public class ProcessorContext { private final MediaType mMediaType; private final HtmlRenderer mHtmlRenderer; private final SyntaxHighlighter mHighlighter; public ProcessorContext( final MediaType mediaType, final HtmlRenderer htmlRenderer ) { this( mediaType, htmlRenderer, new PlaintextHighlighter() ); } public ProcessorContext( final MediaType mediaType, final HtmlRenderer htmlRenderer, final SyntaxHighlighter highlighter ) { mMediaType = mediaType; mHtmlRenderer = htmlRenderer; mHighlighter = highlighter; } MediaType getMediaType() { return mMediaType; } HtmlRenderer getHtmlRenderer() { return mHtmlRenderer; } SyntaxHighlighter getSyntaxHighlighter() { return mHighlighter; } } Adding the new member variable allows any text processor to interact with a syntax highlighter without any modifications to the ProcessorFactory class, which was the main driving force behind creating the ProcessorContext class. Note that record classes provide a terse syntax for declaring data holders, such as the ProcessorContext class, that serve to transport immutable data, exclusively. As it stands, this class is a suitable record class candidate. At the time of writing, record classes are a preview feature of the Java language, which can be used when preview features are enabled. Records have been integrated into Java 16. Let’s return to the App class, where we’ll construct a new ProcessorContext class by giving it a suitable syntax highlighter. Wiring the Application Let’s stitch the classes together by giving the syntax highlighter to the processor context. To do so, first find the following line in the App class: final var context = new ProcessorContext( TEXT_MARKDOWN, preview ); Then replace that line with the following lines: final var syntax = HighlighterFactory.create( TEXT_MARKDOWN, editor ); final var context = new ProcessorContext( TEXT_MARKDOWN, preview, syntax ); Even though the ProcessorContext could have created the highlighter internally (by passing in the editor instead of a highlighter), we create the highlighter externally and pass it along so that the code subscribes to the Dependency Injection (DI) principle. Put succinctly, DI means that a parent object is to provide all necessary dependencies for its child objects. We do this so that the concrete implementations used by the child objects can be changed without having to update the child’s code. Complete the edits by loading a Cascading Style Sheet (CSS) file—as yet undefined—that defines the style classes for the text editor. Accomplish this by inserting the following lines after creating the scene instance: final var stylesheet = App.class.getResource( "markdown.css" ); editor.getStyleClass().add( "editor" ); editor.getStylesheets().add( stylesheet.toExternalForm() ); Create an empty CSS file named src/main/resources/com/mdtexfx/markdown.css . The directory portion under resources directly relates to the package name of the com.mdtexfx.App class, from which the resource is obtained via the getResource method. Test that everything works by running App within the IDE. The application is wired up; we’re now ready to fill in the details for the syntax highlighting implementation. MarkdownProcessor & NodeVisitor In flexmark-java, there is a fundamental class called a NodeVisitor, which implements the visitor design pattern with the help of a VisitHandler. Effectively, the NodeVisitor class provides a way to map nodes found in an abstract syntax tree to methods (lambda functions) that process particular node types. For example, we’d like to map a Markdown heading node (e.g., ## Heading 2) to a distinct heading style class, such as h2, so that the text editor can apply the associated CSS effects to the marked text. Consider the following pseudo-code snippet: visitor = new NodeVisitor( new VisitHandler( Heading.class, node -> syntax.highlight( node.start(), node.end(), "h" + node.level() ) ) ); visitor.visit( root ); A few items to notice: The NodeVisitor constructor takes a variable number of VisitHandler instances. A VisitHandler can map Node subclasses to lambda functions that are called when the NodeVisitor visits any node of that subclass type. Calling start() and end() represents obtaining the offsets of where the node starts and ends within the document; in practice, these functions don’t exist—instead, we’ll use equivalent method calls that correspond to the node type being visited. Calling visit( root ) takes the root node from the abstract syntax tree that was already parsed when we ran the Markdown processor to generate an HTML document (for previewing). Moving from pseudo-code to actual code involves changing the MarkdownProcessor class as follows: public class MarkdownProcessor extends ExecutorProcessor<String> { private final IParse mParser = Parser.builder().build(); private final IRender mRenderer = HtmlRenderer.builder().build(); private final SyntaxHighlighter mSyntax; private final NodeVisitor mVisitor; public MarkdownProcessor( final Processor<String> successor, final ProcessorContext context ) { super( successor ); mSyntax = context.getSyntaxHighlighter(); mVisitor = new NodeVisitor( create( Text.class, node -> container( node, "text" ) ), create( Heading.class, node -> heading( node, "h" + node.getLevel() ) ), create( Code.class, node -> delimited( node, "code" ) ), create( Emphasis.class, node -> delimited( node, "emphasis" ) ), create( StrongEmphasis.class, node -> delimited( node, "strong" ) ), create( FencedCodeBlock.class, node -> fenced( node, "pre" ) ), create( BlockQuote.class, node -> container( node, "blockquote" ) ), create( BulletListItem.class, node -> itemized( node, "bullet" ) ), create( OrderedListItem.class, node -> itemized( node, "enumerated" ) ), create( Link.class, node -> link( node, "link" ) ), create( HtmlEntity.class, node -> container( node, "entity" ) ) ); } @Override public String apply( final String markdown ) { final var root = mParser.parse( markdown ); mVisitor.visit( root ); return mRenderer.render( root ); } A few techniques are employed to make the code readable and eliminate a bit of duplication: The create method instantiates a VisitHandler. The container, heading, delimited and similar methods hide how the node document offsets are obtained and subsequently applied via the syntax highlighter. The style class strings (e.g., code, emphasis, blockquote) are clearly listed at a glance, which will help when developing the CSS file. Download the source code at the end of the article to read how each type of node is mapped to specific document offsets. The code to handle styling is complete. Markdown Cascading Style Sheet The functionality for JavaFX Cascading Style Sheets is based on the World Wide Web Consortium’s CSS specification version 2.1. Note that most style names start with -fx-, such as -fx-fill and -fx-font-size. Here are some salient parts to the markdown.css file that we created previously: .editor { -fx-background-color: #fdf6e3; } .editor .code, .editor .pre { -fx-fill: #002b36; -fx-font-family: monospace; } The CSS file contains mappings for the style class names passed into the lambda expressions we listed in the MarkdownProcessor class. When referenced in CSS, the names must be prefixed with a period. We’ve also introduced an editor style class name as a qualifier to help avoid conflicts with other CSS styles. Keep in mind that all style attributes, such as -fx-fill, are defined specifically for JavaFX; in some cases, the rich text editor requires the -rtfx- prefix for styles to stick. When the application is run, the syntax highlighting will resemble the following image: Download You may download the complete project. Summary We’ve seen how the visitor pattern can decouple visiting all nodes in an abstract syntax tree from the code that handles the processing of each visited node. We also looked at a few key differences between the JavaFX CSS and the W3C CSS specifications. Lastly, we used a top-down software development approach by starting with skeleton classes and interfaces, wiring them together, then filling in the low-level implementation details afterwards. In the next article, we’ll add TeX support. Related posts TeXnical Writing Part 1: Foundations TeXnical Writing Part 2: Markdown TeXnical Writing Part 4: Math TeXnical Writing Part 5: Performance - [BellSoft releases Liberica Native Image Kit](https://bell-sw.com/announcements/2021/02/19/BellSoft-releases-Liberica-Native-Image-Kit/): Liberica Native Image Kit (Liberica NIK) is a utility that converts your JVM-based application into a fully AOT compiled native executable under the closed-world assumption with an almost instant startup time. It is based on the open source GraalVM Community Edition. Being compatible with various platforms, including lightweight musl-based Alpine Linux, this technology optimizes resource consumption and minimizes the static footprint. In this article, we’ll demonstrate how Liberica NIK aids in software development and what problems it solves. Discover Liberica NIK A versatile optimization tool Native Image is a helpful technology to accelerate your applications. In one of the recent articles, we ran a test to see which Java microservice configuration would use the least amount of RAM and launch the fastest. Compared to other methods, running a project in a native image on Alpine Linux consumed half as much memory and started up in 1/10 of a second, more than ten times as quickly as with JVM optimizations. At that time, it was only possible to execute GraalVM on Alpine with glibc installed separately. However, the whole purpose of using Alpine is to take advantage of the musl library. Bringing full musl support to GraalVM Open Source was a challenge that BellSoft took. We have contributed four patches, two of which are integrated and two are under review as of writing this article. BellSoft’s contribution to GraalVM — Linux-musl support Linux-musl support #230 (integrated) Added linux-musl support #175 (fastr) (integrated) [WIP] linux-musl support #3141 (under review) Added linux-musl support #2223 (truffleruby) (under review) All the patches deal with adding support for Linux distributions that use musl as their standard C library such as Alpine. They are meant to be added to both Graal’s build system and code. BellSoft team came forth to introduce these changes in order to enable its planned musl-based GraalVM builds of Liberica JDK, a free and 100% open source OpenJDK distribution. Just like our JEP 386 integrated into JDK 16, musl support provides three image-related benefits: All these bring considerable economy in terms of time, traffic, and financial expenses. “Less is more” perfectly applies to IT: By minimizing resources, your project maximizes business value. See how much the Alpine + native image combination saves in footprint, RAM, and speed in the next section. Liberica Native Image Kit We release pre-assembled packages dubbed Liberica Native Image Kit that contain Liberica VM, native image (derived from GraalVM CE) and language installables compatible with a specific operating system. Click the button below to open the Download Center and choose a package compatible with your system. Download Liberica NIK The current builds have the following features: OS: Linux x86_64 (glibc), Linux Alpine x86_64 (musl), Linux AArch64 (glibc), Linux Alpine AArch64 (musl), Mac OS 64_64 are supported Languages: Java, JavaScript, LLVM, Python, Ruby, R, and WebAssembly Update level: JDK 11.0.10, Graal VM CE 21.0 Learn more about which languages are available on different platforms at the Supported Configuration page. Let’s show the gains you get with this tool through an experiment. A Spring-based echo microservice was first compiled into a JAR file and run inside a Debian 9 container. Then we compiled it to both a glibc-based native image (run inside a Debian 9 container) and a musl-based one (run inside an Alpine 3.11 container). The comparative results are shown in the table below. Another prominent advantage is that Liberica NIK allows seamless polyglot projects, such as microservices in different programming languages. To understand the impact of using native images with Alpine Linux we created a simple client in Node.js and a simple server with Spring, mimicking a microservice application. public class EchoApplication { public static void main(String[] args) { SpringApplication.run(EchoApplication.class); } @PostMapping("/") public String echo(@RequestBody String body) { return body; } } Installation guide We will give instructions separately for all Linux distributions (including Alpine) and macOS. Use one of the following links, depending on your platform. Alpine Linux Download a Liberica NIK package from BellSoft: wget https://download.bell-sw.com/vm/21.0.0.2/bellsoft-liberica-vm-openjdk11-21.0.0.2-linux-x64-musl.tar.gz wget https://download.bell-sw.com/vm/21.0.0.2/native-image-installable-openjdk11-21.0.0.2-linux-x64-musl.jar Specify the installation directory for this package: export INSTALL_DIR= Unpack the package: tar -C $INSTALL_DIR -xzf Configure your environment: export NIK_HOME=$INSTALL_DIR/liberica-vm-openjdk11-21.0.0.2 export PATH=$NIK_HOME/bin:$PATH Finally, install Native Image with gu -L install native-image-installable-openjdk11-21.0.0.2-linux-x64-musl.jar Linux Download a Liberica NIK package from BellSoft: wget https://download.bell-sw.com/vm/21.0.0.2/bellsoft-liberica-vm-openjdk11-21.0.0.2-linux-amd64.tar.gz wget https://download.bell-sw.com/vm/21.0.0.2/native-image-installable-openjdk11-21.0.0.2-linux-amd64.jar Specify the installation directory for this package: export INSTALL_DIR= Unpack the package: tar -C $INSTALL_DIR -xzf Configure your environment: export NIK_HOME=$INSTALL_DIR/liberica-vm-openjdk11-21.0.0.2 export PATH=$NIK_HOME/bin:$PATH Finally, install Native Image with gu -L install native-image-installable-openjdk11-21.0.0.2-linux-amd64.jar macOS Download a Liberica NIK package from BellSoft: curl -O https://download.bell-sw.com/vm/21.0.0.2/bellsoft-liberica-vm-openjdk11-21.0.0.2-macos-amd64.zip curl -O https://download.bell-sw.com/vm/21.0.0.2/native-image-installable-openjdk11-21.0.0.2-macos-amd64.jar Unpack the package: unzip bellsoft-liberica-vm-openjdk11-21.0.0.2-macos-amd64.zip Move the package to libraries: sudo mv bellsoft-liberica-vm-openjdk11-21.0.0.2 /Library/Java/JavaVirtualMachines Configure your environment: export JAVA_HOME=/Library/Java/JavaVirtualMachines/bellsoft-liberica-vm-openjdk11-21.0.0.2/Contents/Home export PATH=$JAVA_HOME/bin:$PATH Finally, install Native Image with gu -L install native-image-installable-openjdk11-21.0.0.2-macos-amd64.jar Language support With this product release, BellSoft introduces support for multilingual projects as an experimental feature. Here’s how you use Liberica language binaries: Open the terminal and type gu available. You’ll see a list of available languages: Downloading: Component catalog from download.bell-sw.com ComponentId Version Component name Stability Origin -------------------------------------------------------------------------------------------------------- llvm-toolchain 21.0.0.2 LLVM.org toolchain Supported download.bell-sw.com native-image 21.0.0.2 Native Image Early adopter download.bell-sw.com python 21.0.0.2 Graal.Python Experimental download.bell-sw.com R 21.0.0.2 FastR Experimental download.bell-sw.com ruby 21.0.0.2 TruffleRuby Experimental download.bell-sw.com wasm 21.0.0.2 GraalWasm Experimental download.bell-sw.com Execute gu install [language] to install specific language. For example, gu install python. This is also possible to execute manually: wget gu -L install .jar For example, wget https://download.bell-sw.com/vm/21.0.0/python-installable-openjdk11-21.0.0-linux-amd64.jar gu -L install python-installable-openjdk11-21.0.0-linux-amd64.jar Are you keen on using native image in your own business but feel unprepared? Don’t worry, switching to a new tool is never hard or stressful with a reliable partner to support you. We have put together a couple of case studies and will be happy to show how our clients apply Liberica NIK to their enterprise projects. Besides saving you time, they might contain some elegant solutions for your use case. Click the button below, fill out the form, and one of BellSoft’s senior engineers will get back to you soon. Book a free consultation Conclusion Next time we’ll go further and talk about different ways of using containerized applications executed with Liberica NIK. You see that with this tool at your disposal, software components consume less RAM, communicate with total ease, and run at high speed. We are happy to bring the technology to the OpenJDK community and look forward to exploring more applications for musl-based solutions. - [BellSoft’s 2020 in numbers](https://bell-sw.com/announcements/2021/02/24/BellSofts-2020-in-numbers/): Many would agree that 2020 was a challenging year for the global communities. We at BellSoft welcome the challenge; it’s our second nature! It turns out, last year was a big one for us. Compared to 2019, we almost doubled the number of users and more than tripled downloads. People are discovering new aspects of Liberica JDK, while our engineering team (which is 1.5 times bigger now) never stops adding new exciting features. Our progressive open source Java runtime interests not only private developers but large corporations, too. Last year we signed important support contracts with Pivotal, SoftChalk, and Flow Traders. BellSoft found new partners in VMware, with whom we work on improving the Java platform and develop the VMware Tanzu user community. Another big collaboration was with Karakun on a new bundled product: Liberica JDK with OpenWebStart. Furthermore, we continue our partnership agreement with JetBrains to provide critical patches and security updates for JetBrains Runtime. We consider technology standards to be very important in keeping Java™ and its community vibrant. And easily accessible technology standards help the ecosystem thrive. Thanks to that adherence, in November 2020, our CTO Aleksei Voitylov was elected to the Java Community Process (JCP) Executive Committee, who essentially guide the JDK evolution. Here’s our statement: BellSoft is ready to become a broadcaster for those who are routinely working with the language first-hand. We’ll try to translate your voices into actions, maintaining the transparent and dependable nature of the communication process and promoting technological development based on the needs of the OpenJDK community. Discover Liberica JDK Liberica JDK releases are concurrent with Oracle Java SE, and last year we pushed six new versions of our product. With the first update of 2020, we introduced a unique Lite flavor optimized for microservices and high-density deployments in clouds. Together with the Alpine Linux port integrated into the upcoming JDK 16 with full native support for the musl C library, this development helped us achieve the smallest Docker container on the market. We kept on reducing the size of this tiny Liberica JDK image throughout the year. Small container images are indispensable in production: they cut down storage expenses and increase deployment speed immensely. Now, BellSoft’s microcontainer is 1/7 the size of the standard one, and it’s only the beginning! In 2021, we’ll keep the momentum going: grow the business, increase the user base, and innovate ahead of market trends. Our unique microcontainer will become even more lightweight (thanks to reduced static and dynamic footprint), and Liberica JDK will get supported on more platforms, including Apple M1, AArch64, and AWS. The global plans involve expanding to Asia and launching an internship program for young and talented Java developers. And of course, we will always deliver High-Powered Support and be a trusted partner to our customers. Request Licensing “We would like to thank all of BellSoft’s clients, millions of Liberica users, and the OpenJDK community for trusting us and choosing our products in 2020. We are happy to contribute to the incredible global expertise in Java. As world-class JDK experts, we know that disregard for proper IT hygiene can lead to perverse outcomes: system malfunctions, compromised data or leaked data, both corporate and personal. That’s why we are working on truly innovative ideas that we share with the industry. We do our utmost to provide customers with fast and professional support, high confidence, and tools to ensure the benchmark security of the Java platform,” says Alex Belokrylov, the CEO of BellSoft. - [Liberica JDK offers native Java builds for Apple Silicon M1](https://bell-sw.com/announcements/2021/03/12/Liberica-on-Apple-Silicon/): As of January 2021, Liberica JDK runs natively on Macs powered by the first processor of Apple’s design specifically for Macintosh computers, M1. This feature applies to both LTS’s (8, 11, 17) and the current version. 18 months have passed since then. Apple extended the line of M1 processors, each more powerful than the previous one. But as a Java developer, you must primarily be interested in practical things: Are there any compatibility issues? Are all solutions optimized for Apple M1? Are there any differences in installing and running software? Which applications will benefit most from Apple silicon? Find the answers to these questions and more in our article! Thinking different with Apple silicon What do we understand by this concept? Well, it’s three things in one, each with its weight. Primarily, it refers to Apple-designed chips and the company’s first ARM-based SoCs (systems on a chip) for Macs called M1. They are intended to replace the lineup of Core processors. Some people would mean specific devices with the said processor: Mac mini, as well as MacBook Pro 13 and MacBook Air, both Late 2020. The third possible denomination builds upon the macOS Big Sur and newer. In this case, Apple silicon should be viewed as a hardware + software combination designed for ultimate speed and performance. The overarching characteristic here is a transition to the arm64 architecture. This is right up BellSoft’s alley — we’ve been exploring this technology for many years (read more in my article for Java Magazine, Sep/Oct 2018). Its key difference between ARM and x86 CPUs is in their methods for building instruction set architectures (ISAs): CISC and RISC. While the first design, Complex Instruction Set Computing, focuses on complicated instructions to encode more than one operation, RISC (Reduced Instruction Set Computing) based one uses fixed-length instructions, each performing a single operation. Thus, ARM is optimal for mobile devices, and x86 is perfect for desktops. But everything is changing. When arm64 moves to desktops, it shows significant advantages: with ARM lower TDP (Thermal Design Power) per core, M1 is much more energy-efficient early benchmark tests already show how the new chips outclass Intel-made processors in previous Macs Here’s an illustration of why we need a native JVM for Apple silicon. We took several DaCapo benchmarks using default JVM parameters on two implementations: macOS-x86_64 on Rosetta 2 and macOS-aarch64. A few times difference in performance is the answer. Family of Apple silicon chips Apple silicon reveals new capacities with each new product in the line. In addition to the basic M1 chip, Apple released M1 Max in 2021, and M1 Ultra and M1 Pro in 2022. Both M1 Max and M1 Pro feature up to ten CPU cores with eight performance cores and two efficiency cores , but M1 Pro includes 16 GPU cores, whereas M1 Max has twice as much. M1 Pro stands between plain M1 and M1 Max chips in terms of performance and is suitable for most professional tasks. But users who have to deal with demanding GPU workflows such as 3D modeling, video editing, etc., will benefit more from the M1 Max, which also has two video encode engines as compared to M1 Pro with only one. M1 Ultra is the most powerful chip in the family. It features 20 CPU cores and 64 GPU cores as It is essentially two M1 Max chips connected together with a silicon interposer — the technique helped to avoid performance tradeoff and increased power consumption. So the new chip boasts doubled capabilities of M1 Max and can be considered the most powerful PC chip in the world (although due to its size, M1 Ultra is utilized only in the Mac Studio): The Geekbench 5 benchmark demonstrated M1 Ultra multi-core performance results of 24,055, which is three times higher than M1 results and almost two times higher than M1 Max 128GB of unified memory as compared to max 48 GB in the most powerful PC graphic cards deliver the most impressive results for intensive GPU workflows The 32-core Neural Engine running up to 22 trillion operations per second is fit for the most demanding machine learning tasks Thanks to four video encode engines, M1 Ultra is the only chip in the world that can play back up to 18 streams of 8K ProRes 422 video In June 2022, the company introduced one more Apple silicon family member — the M2 chip. It nestles cozily in-between M1 and M1 Pro. Apple claims it has an 18% increase in CPU performance, a 35% increase in GPU performance, and a 40% more performant Neural Engine as compared to the M1 chip. Apple’s expansion of the processor family means, firstly, that the research continues and we may expect even more powerful chips in the future, and secondly, that any user can choose a device perfectly suitable for their needs. Liberica JDK on M1 How to install Java on macOS (x86 or ARM) Liberica JDK for M1 is a production-ready TCK-verified binary. Try BellSoft’s progressive Java runtime for modern deployments and fully experience its benefits for macOS users. Download Liberica JDK for Apple M1 For the latest JDK release on macOS, BellSoft offers a choice between Liberica JDK for x86 and ARM. However, we recommend installing both runtimes on your M1 device in early 2021. How to install both Liberica JDK for x86 and ARM Download the .zip package and unpack it. Change the name of the top-level directory to jdk-xx.xx.xx.jdk (e.g. jdk-11.0.10.jdk). wget https://download.bell-sw.com/java/11.0.10+9/bellsoft-jdk11.0.10+9-macos-amd64.zip unzip bellsoft-jdk11.0.10+9-macos-amd64.zip Download the second archive and unpack it without changes. wget https://download.bell-sw.com/java/11.0.10+9/bellsoft-jdk11.0.10+9-macos-aarch64.zip unzip bellsoft-jdk11.0.10+9-macos-aarch64.zip DO NOT download and install DMG files if you want to run both x86 and ARM builds on one JDK version. Alternatively, you may use DMG to install different versions of Liberica JDK (e.g. 15.0.2 for ARM + 11.0.10 for x86 ). Learn more about this process in the Install Guide. Why are native builds important? Java™ is a cross-platform language, meaning that all Java applications should run on every system available. But large software often has native code compiled for a specific architecture, including x86_64, which had been the only option in desktop and laptop Macs. Another reason is that many app developers have not yet moved their products to ARM. They will eventually. Apple itself expects the transition to take about two years, during which it will still produce Intel-based machines. For the time being, older versions of software built for x86 can run under a special translation environment, Rosetta 2, bundled with Big Sur. This virtualization tool, in fact, makes both x86 and ARM builds of Liberica JDK work simultaneously. Support for JVM features: all necessary garbage collectors C1 and C2 just-in-time compilers required for normal operation Docker The latest Docker versions are compatible with Apple silicon. Docker Desktop supports multi-platform images, so there is no need for a complex cross-compilation environment in case you want to build images for both platforms: x86 and ARM. In addition, the docker buildx command enables seamless integration of multi-platform builds into the project. Compatibility issues are being gradually eliminated. However, there are still cases when you need to use Rosetta 2: Some command line options (the old version of 1x docker compose, docker scan, and docker-credential-ecr-login) do not work without translator software Some images do not support ARM64 yet ping from inside a container to the Internet is associated with unexpected behavior Occasional data drop with a half-closed TCP stream It is recommended to use containers built for ARM64 whenever possible to avoid issues and performance deterioration. Use Docker Hub to identify images supporting ARM64. GraalVM Starting with GraalVM version 22.1, the technology is compatible with ARM64. The components built on JDK11 or JDK17 include JVM with libgraal, native-image, JavaScript, Java on Truffle (Espresso). Liberica Native Image Kit, a tool based on GraalVM CE, is also available for Apple silicon. Unlike GraalVM CE that comes only as .tar.gz package, Liberica NIK can also be downloaded as .dmg with a convenient Installator. In addition, Liberica NIK comes in three versions: Full with LibericaFX (an instance of OpenJFX), Standard with plugins for non-Java languages, and Core with Liberica VM and native-image without support for language installables. Technologies not yet optimized for Apple silicon Although the software optimization for Apple M1 chips is in full swing, some applications including VirtualBox still do not support the new architecture. A solution would be to search for alternatives. For instance, VirtualBox can be substituted with VMware Fusion. Other utilities require Rosetta 2 to function properly: MongoDB Compass, MongoDB, Apache Maven, MySQL Workbench, etc. A comprehensive compatibility table can be found here. Features specific to Liberica JDK BellSoft provides a notarized runtime, which allows notarizing your Java or JavaFX applications. To learn more about how to use Liberica JDK with the Javapackager tool, follow our detailed notarization guide Serviceability Agent API in JDK 8 to monitor VM’s internal operations, a handy tool for advanced Java development Full Liberica JDK binaries include JavaFX for complex and appealing visual interfaces, complete with OpenJFX Graphics, Media (video and audio), Controls, and Webkit modules Conclusion The ARM architecture has been a prominent trend during the past couple of years. We feel fortunate to have predicted its rise back in 2018. Thanks to that, BellSoft is now one of the few OpenJDK contributors who are actively involved in advancing Java on this platform. Apple M1 promises massive power shifts in the IT industry, and we’re looking to the future with interest and healthy optimism. While macOS app developers are slowly transitioning from x86, you may count on Liberica JDK to run seamlessly on every device and have your business covered. - [Liberica JDK 16: more developer-friendly, more powerful](https://bell-sw.com/announcements/2021/03/19/JDK-16-Release/): Today we welcome JDK 16, the last version of the Java Development Kit before the stable, long-term support (LTS) release. Contrary to what some people would assume, it’s not a “placeholder” title. Version 16 is a standalone JDK and features 17 new capabilities — with native support for Alpine Linux integrated by BellSoft. There were 3085 bug fixes, backports and sub-tasks performed, including 21 issues resolved by the BellSoft team. OpenJFX also got 58 bugs resolved prior to this release. Here’s the official JDK 16 fix ratio from Oracle: In the following paragraphs, we’ll explore our personal favorites in JDK 16, ranked by their value to user-end Java developers. Download Liberica JDK 1. Records (JEP 395), Sealed Classes (JEP 397), and Pattern Matching for instanceof (JEP 394) The combination of these three features is an entirely new step toward algebraic data types in Java™. They are essentially the next iteration of Project Amber. First previewed back in JDK 14 (by JEP 395), records act as transparent carriers for immutable data. They answer the language’s verboseness and stringent nature (which sometimes leads to boilerplate). Sealed classes got a second preview in this version; they were proposed by JEP 360. A class or an interface is sealed with the sealed modifier to its declaration and restricts which other classes or interfaces may extend it. Lastly, pattern matching, previewed by JEP 305 only for the instanceof operator, for the time being, allows common logic in a program to be expressed more concisely and safely. The work here is still in progress, and we’re patiently waiting for the entirety of Pattern matching to be delivered in some future JDK versions. Overall, records + sealed classes + pattern matching enhance Java and make it even more convenient to describe algorithms. Here’s a nice example: // sealed requires --enable-preview --source 16 sealed interface Expr permits SumExpr, NegExpr, IntExpr { } record SumExpr(Expr left, Expr right) implements Expr { } record NegExpr(Expr expr) implements Expr { } record IntExpr(int value) implements Expr { } static int eval(Expr e) { // Let's wait for pattern matching for switch // Let's wait for records deconstruction if (e instanceof SumExpr se) return eval(se.left) + eval(se.right); if (e instanceof NegExpr ne) return -eval(ne.expr); if (e instanceof IntExpr ie) return ie.value; return 0; // Should become unnecessary (not yet in recent EA) } 2. Vector API (JEP 338) One of the most meaningful features in this release, albeit still in incubating form (jdk.incubator.vector). It’s about enabling advanced developers to code cross-platform computations leveraging hardware data parallelism. Vector API explicitly deals with the following dark silicon instructions that JVM and JDK weren’t able to leverage previously: SIMD; AVX on x86; NEON and SVE on ARM. The goals for this JEP were to develop a clear and concise API that is platform-agnostic, performing reliably and degrading gracefully. It was also meant to be maximally expressive and portable, abstracting away from different peculiarities of instructions in architecture. The resulting Vector API allows fast mathematical calculations in Java. It is an out-of-the-box functionality running natively both on Intel-based and ARM-based devices. In the future, we expect Vector API to benefit from value types once they are ready and roll out with Project Valhalla. 3. Alpine Linux Port (JEP 386) This JEP was the most exciting for our team because we have been among its creators. Now that the Alpine Linux port has met upstream, everyone can start building tiny containers with a lightweight OS image and your JDK of choice. Of course, we recommend Liberica JDK since it has been optimized for this platform for quite some time! Download Liberica JDK for Alpine Linux Read more about the benefits of Alpine Linux native support and our engineering work on JEP 386 in last year’s article “Optimizing Java Microservices with the smallest container on the market”. Or watch this interview with Dmitry Chuyko, Senior Performance Architect at BellSoft, where he talks at length about the port and its meaning for Java development. 4. Windows/AArch64 Port (JEP 388) Another solid contribution from an OpenJDK vendor, this time the Java development team at Microsoft. The Windows port for ARM is extended from the Linux/AArch64 port (JEP 237), has got optimized intrinsics from JEP 315, and is finally ready for production. Needless to say, this feature has been highly expected in the community. Its integration to the mainline JDK will lead to more available Windows-based binaries for ARM 64-bit processors. 5. Packaging Tool (JEP 392) Until recently, the recommended way to use the Java language on a system was to have a single shared runtime on which to run all Java programs. This was meant to save space on your hard drive and bandwidth by downloading only the bytecode instead of the full runtime. Unfortunately, Java applications were then delivered as JARs, which meant: you first needed to install the runtime, then the JAR, and somehow add the JAR to the runtime. This is not enough for the modern application developer. They must deliver quality UX with installables suited for the user platform. All programs should behave in a familiar manner (like when double-clicking on a package launches a wizard on Windows or drag-and-dropping a file into the Application folder installs it on macOS). The utility (proposed initially by JEP 343 in JDK 14) builds on the legacy javapackager from Oracle JavaFX and introduces the production-ready jpackage tool. It allows developers to create their own self-contained Java applications with runtime included. The feature is supported for all operating systems and their native packaging formats: msi and exe on Windows, pkg and dmg on macOS, and deb and rpm on Linux. Would you like to learn how these new features can help your Java software but feel uncertain about switching to JDK 16? No need to worry! Let our expert engineers guide you through the migration process and receive High-Powered Support from a major OpenJDK contributor. Book a free consultation 6. Elastic Metaspace (JEP 387) Even since the JDK project moved to open source, the community has been teaching garbage collectors how to return unused buffer memory to the operating system. It was implemented in JDK 12 for the default G1. Then, two new low-latency GCs, scalable ZGC (first integrated into JDK 11) and low-time-pause Shenandoah GC (first integrated into JDK 12), changed from experimental features to product features in JDK 15. These collectors attempt to tackle the common problem of too long full GC pauses and increase the overall responsiveness in the cloud. This JEP is about working with the internal Java structures so that they also give back some of the consumed resources. Previously, when the memory of metaspace collecting HotSpot class metadata was no longer needed, it was returned to the JVM. But the JVM itself did not return it to the OS promptly. Now, with metaspace elasticity improved, this memory gets allocated in smaller chunks, while also decreasing class-loader overhead and fragmentation. It means returning unused memory faster, reducing footprint, and cutting down on maintenance costs. We thank our colleagues at SAP for the valuable contribution to OpenJDK! 7. Enable C++14 Language Features (JEP 347), Migrate from Mercurial to Git (JEP 357), and Migrate to GitHub (JEP 369) Not all improvements manifest themselves in the binaries used for app development. Some JEPs are geared toward the developers of Java as the programming language. However, the rest will benefit indirectly since these changes make it easier to get work done for the community and deliver enhancements faster. We’re glad that the migration to Git is complete, that we can use C++ features, and that GitHub’s modern development and code review tools are finally available to all contributors to the JDK core. Much thanks to the Oracle team and Oracle-led Project Skara. 8. Strongly Encapsulate JDK Internals by Default (JEP 396) The creators of Java provide a set of APIs that developers are welcome to apply in their production and testing. There is an unvoiced agreement that the future versions of the JDK will continue having this toolset. In order to deliver those APIs, the maintainers of the platform have internal elements of the JDK, which weren’t initially meant for external purposes. However, many end-user Java applications appear to be tied to the internal JDK APIs and not public ones. Ever since JDK 9, the efforts to hide them from public use have been implemented within Project Jigsaw. Developers are encouraged to migrate from using internal elements to using standard APIs in JDK 16 with this strong encapsulation: access to packages that existed in JDK 8 and do not contain critical internal APIs will be denied. Migration to 16 thus may be harmful because some libraries (e.g., Lombok) are not prepared for this change — and it’s been four years after JDK 9! What should you do if your software depends on APIs from this list of internal packages no longer open by default? Rewrite the application without referring to these elements; While you’re at it, use the internals with special –illegal-access keys described in the JEP. Overall, this switch will improve the security and maintainability of Java SE implementations in general, and the JDK in particular. Custom Liberica JDK features We could not leave this major release without introducing something special for our clients. Discovery API v1 is still the actual stable version. All new Liberica JDK binaries are available with the same API. Spring Boot container default. A perk that has actually been around before this release: When building a standard Docker container in Spring with the Maven plugin, it automatically picks Liberica JDK as a base runtime image. Continued support for AOT and Graal JIT. These experimental features have been deprecated in Oracle’s OpenJDK builds and are now subject to removal. We at BellSoft are advocates of the native image technology and recommend compiling native images with Liberica NIK to avoid any errors. The OpenJDK community is stronger and closer than ever in bringing efficiency to software engineers across the world. Now let’s buckle up and get ready for JDK 17, due to roll out this September! To download all the Liberica JDK 16 builds, click here or the button below. Download Liberica JDK - [Get savvy about native image at JRush](https://bell-sw.com/events/2021/03/25/Events-JRush/): In the light of the Liberica NIK release, BellSoft invites high-level software developers and Java enthusiasts to discover native image. Join us on March 25 at 11 am PDT online. Learn how this wonderful tool can optimize your projects and minimize resources! We introduce a new web conference series: JRush is where you’ll get the densest and most concise information on a specific topic. Our first choice is the native image technology and its associated tools. In just 2 hours, you will get your fix on native image from the top VMware and BellSoft experts. Watch a beautifully crafted introduction to the Spring Native project by its co-creator Andy Clement and Spring Developer Advocate Josh Long. Meet Dmitry Chuyko, our Senior Performance Engineer, who will unravel what you will achieve with native image and tiny microservice containers on Graal. Register now — Rush with us! Register for JRush See you there! - [TeXnical Writing Part 4: Math](https://bell-sw.com/announcements/2021/03/26/TeXnical-Writing-Part-4-Math/): Welcome back! In the previous part of this series we developed a plain Markdown editor and preview panel; in this part we’re going to focus on rendering equations and formulas in our application based on Liberica JDK. Discover Liberica JDK Before we jump in, let’s review a brief history of mathematical typesetting followed by some important terminology that we’ll encounter later on in this article. Introduction Before computer-based typesetting, much of mathematics was put to page by hand. Professional typesetters, who were often expensive and usually not mathematicians, would inadvertently introduce typographic errors into equations. Phototypesetting technology improved upon hand-typesetting, but well-known computer scientist Donald Knuth—whose third volume of The Art of Computer Programming was phototypeset in 1976—expressed dissatisfaction with its typographic quality. He set himself two goals: let anyone create high-quality books without much effort and provide software that typesets consistently on all capable computers. Two years later, he released a typesetting system and a font description language: TeX and METAFONT, respectively. In TeX, a control sequence is a backslash followed by some text; TeX defines about 900 control sequences. Example control sequences are: \frac, \", \hskip, and \input. These control sequences are further categorized into control words, control symbols, primitives, registers, macros, and more. A macro, short for macro instruction, is a control sequence that invokes one or more control sequences to perform a task. TeX is powered by a sophisticated macro language that forms the foundation for larger TeX engines—such as LuaTeX, pdfTeX, and XeTeX—as well as for document formats like ConTeXt and LaTeX. We’ve seen how structured plain text formats—AsciiDoc, Markdown, and similar—separate content from presentation. In practice, documents written in LaTeX typically inextricably intertwine prose with formatting; ConTeXt makes keeping the two separate much easier. In this spirit, we’re going to integrate a subset of TeX control sequences that are not tied to a specific format (e.g., ConTeXt or LaTeX), to keep open the possibility of using any engine to typeset and stylize a document written in a structured plain text format. We only need a subset because displaying a simple equation covers the most common use case; integrating a complete TeX typesetting engine is well outside of our project’s scope. Java TeX Engines Having opted to develop a Java-based Markdown editor with JavaFX, using a free, open-source, pure Java solution to convert TeX macro equations into SVG images will simplify application development and distribution. At first blush, there are a number of candidate implementations. Let’s take a closer look at the high-level technical details of each to assess their suitability for inclusion. New Typesetting System The New Typesetting System (NTS) is a rewrite of TeX intended to be functionally compatible while offering a modular framework for experimentation and extensions. This implementation would have to be modified to export equations in a vector format. NTS produces page-oriented output for DeVice Independent (DVI) files, so we’d need a second pass to convert from DVI to SVG. Even though libraries exist for such a task, a single pass is preferred because we’d like to render thousands of equations in real time—two passes may be too slow, architecturally. εχTeX εχTeX is an incredible implementation that builds upon the experiences of developers involved with NTS. When asked about SVG output, the project maintainer suggested creating a new back-end: Maybe [start with] the rudimentary [PostScript] back-end. […] I assume that the level of abstraction is too low for a clean and readable SVG. Thus I would define or redefine TeX/LaTeX/ConTeXt macros — something along the lines of the Unit modules. Instead of passing control too [deeply] into the (unfinished) typesetter, I would generate the output earlier. Or write a new typesetter… Despite thorough documentation and being coded in idiomatic Java, writing our own typesetter from scratch would be a tremendous undertaking. As we’ll soon see, there are alternatives that require much less effort. javaTeX The name javaTeX was given to two different implementations: javaTeX by Carsten Hammer, which provides a viewer for the output from NTS. javaTeX by Timothy Murphy, which converts Knuth’s TeX implementation to Java. Neither of these implementations will produce SVG output without modifications. Murphy states that his implementation is too slow, but that was 1998. Both Java virtual machines and computer hardware have sped up significantly since then, but let’s keep reviewing. JMathTeX JMathTeX, developed at Ghent University, can typeset a subset of TeX control sequences using Java’s Graphics2D API. This is promising. TeX engines, in essence, convert control sequences into symbols that are ultimately mapped to glyphs from specific fonts. Those glyphs must be drawn onto a graphics device using primitive line-drawing instructions—regardless of whether the output is a DVI file, a PostScript printer, a Graphics2D object, or an SVG document. Intercepting calls to the Graphics2D API with an object that generates an SVG document is a one-pass solution. JLaTeXMath JLaTeXMath is a fork of JMathTeX that typesets equations beautifully. Unfortunately, it is bound to LaTeX, making no easy path to extract a TeX-only implementation. But, given that it’s based on JMathTeX, it means some improvements—such as font updates and bug fixes—can be back-ported. Assessment There are many ways to integrate TeX: run an external executable, leverage a JavaScript-based solution such as KaTeX, call a C-based implementation via the JNI, make a web service request, and so on. Let’s run with JMathTeX because it’s a pure Java solution and restricts control sequences to those that are compatible with ConTeXt, LaTeX, and other TeX formats. TeX to SVG As a first step towards embedding TeX, let’s write a test that proves we can convert a simple TeX string into a vector graphic string. Begin as follows: Create a libs directory under mdtexfx to store JMathTeX. Download JMathTeX-0.7pre.jar into mdtexfx/libs. Update the dependencies section inside the build.gradle file to include: an older version of the JDOM library; the JFreeSVG library; and all .jar files in the libs directory (i.e., JMathTeX). implementation 'org.jdom:jdom:1.1.3' implementation 'org.jfree:jfreesvg:3.4' implementation fileTree(include: ['**/*.jar'], dir: 'libs') Reload the Gradle file (see the official documentation for detailed instructions). Create a new Java class named com.mdtexfx.processors.tex.TeXProcessor. Right-click the TeXProcessor class name within the editor. Select Generate → Test. Click OK to confirm. We have everything we need now to write a unit test method that generates scalable vector graphic output from a TeX formula. Change the test class to have the following contents: package com.mdtexfx.processors.tex; import be.ugent.caagt.jmathtex.TeXFormula; import org.jfree.graphics2d.svg.SVGGraphics2D; import org.junit.jupiter.api.Test; import java.awt.*; import static be.ugent.caagt.jmathtex.TeXConstants.ALIGN_CENTER; import static org.junit.jupiter.api.Assertions.assertTrue; class TeXProcessorTest { @Test void test_Formula_TeXInput_SvgOutput() { final var formula = new TeXFormula( "e^{i\\pi} + 1 = 0" ); final var icon = formula.createTeXIcon( ALIGN_CENTER, 20f ); final var component = new Component() {}; final var graphics = new SVGGraphics2D( icon.getIconWidth(), icon.getIconHeight() ); icon.paintIcon( component, graphics, 0, icon.getIconHeight() ); assertTrue( graphics.getSVGDocument().indexOf( " ) > 0 ); } } Points of interest include: new TeXFormula( "e^{i\\pi} + 1 = 0" ) — Instantiates a new TeX formula object using a class from the JMathTeX library. This is the entry point for rendering TeX. createTeXIcon( ALIGN_CENTER, 20f ) — Creates a graphical icon in a font size of 20 points. new Component() {} — Provides the TeX icon object with a foreground colour. An improvement to the library would be to pass in an instance of Java’s Color class. SVGGraphics2D — Intercepts graphics primitive calls (e.g., draw straight lines, curved lines, polygons, etc.) to build an SVG document object model (DOM). The interception is handled by the FreeSVG library, which we’ll revisit later for performance reasons. paintIcon — Draws the formula onto the provided SVGGraphics2D context. Eventually, we’ll want to replace this interface with code that’s more flexible in how TeX is rendered. assertTrue — Verifies that the document contains an SVG element. As long as the TeX formula doesn’t contain the literal character sequence elements. Go back to the TeXProcessor class and change the code to the following: public class TeXProcessor extends ExecutorProcessor<String> { private static final String REGEX = "\\$([^\s][^$]*[^\s])?\\$"; private static final Pattern sPattern = Pattern.compile( REGEX ); private static final Component sComponent = new Component() {}; public TeXProcessor( final Processor<String> successor ) { super( successor ); } private String toSvg( final String tex ) { final var len = tex.length(); assert len > 3; final var formula = new TeXFormula( tex.substring( 1, len - 1 ) ); final var icon = formula.createTeXIcon( ALIGN_CENTER, 20f ); icon.setInsets( new Insets( 6, 0, 0, 4 ) ); final var width = icon.getIconWidth(); final var height = icon.getIconHeight(); final var graphics = new SVGGraphics2D( width, height ); icon.paintIcon( sComponent, graphics, 0, 0 ); return graphics.getSVGElement(); } @Override public String apply( String markdown ) { try { return sPattern.matcher( markdown ).replaceAll( ( result ) -> toSvg( result.group() ) ); } catch( final Exception ex ) { return markdown; } } } Draw your attention to the following lines in the toSvg method: extends ExecutorProcessor — Reuses the processing framework already in place. "\\$([^\s][^$]*[^\s])?\\$" — Defines a regular expression that matches a dollar symbol (\\$) followed by a non-whitespace character ([^\s]), followed by zero or more non-dollar symbols ([^$]*), and ending with a non-space ([^s]) that’s followed by another dollar symbol (\\$). The parentheses and question mark instruct the regular expression engine to use non-greedy matching, without which multiple TeX expressions would be treated as a single equation that spans all text between the first and last TeX expressions in the document. An exercise left for the reader is to allow escaped dollar symbols (\$) inside the TeX expression. Pattern.compile( REGEX ) — Pre-compiles the regular expression, which is a minor performance optimization. sComponent = new Component() {} — Defines the “foreground colour” once so it need not be recreated each time. Prefixing the variable with lowercase s is a naming scheme to denote that the variable is static and has possible side-effects; in contrast, stateless and side-effect-free constants use all uppercase. new TeXFormula( tex.substring( 1, len - 1 ) ) — Removes the leading and trailing $, which we’ve asserted must be present. Also, the regular expression will ensure that toSvg is only called for valid TeX expressions. graphics.getSVGElement() — Swaps the document object model for a string, which avoids including the XML prolog and SVG doctype element. Neither is needed when the SVG element is embedded in an HTML document. The last lines of note replace all TeX expressions in the document with the result of calling toSvg on each one, effectively converting all TeX expressions to SVG elements: return sPattern.matcher( markdown ) .replaceAll( ( result ) -> toSvg( result.group() ) ); When the user types an incomplete (or invalid) TeX expression, the above lines will throw an exception. In that situation, the catch clause will return the original document. This means that if one TeX expression has an issue, then all the TeX expressions will not be rendered. A solution would be to iterate over all the matching TeX patterns individually, building up the output document using a series of string append calls. Another solution is to avoid using regular expressions altogether by registering a TeX processor with flexmark-java. Most of the import statements should be determined by the IDE, automatically. For references that cannot be determined, refer to the following snippet: package com.mdtexfx.processors.tex; import be.ugent.caagt.jmathtex.TeXFormula; import com.mdtexfx.processors.ExecutorProcessor; import com.mdtexfx.processors.Processor; import org.jfree.graphics2d.svg.SVGGraphics2D; import java.awt.*; import java.util.regex.Pattern; import static be.ugent.caagt.jmathtex.TeXConstants.ALIGN_CENTER; Let’s return to the TeXProcessorTest class and clean it up. The unit test test_Formula_TeXInput_SvgOutput had code that we pretty much copied into the TeXProcessor. We can now rewrite the test to verify that the higher-level, fully integrated functionality works fine. Replace the test method with the following code: @Test void test_Formula_MarkdownInput_SvgOutput() { final var renderer = new HtmlCapture(); final var context = new ProcessorContext( TEXT_MARKDOWN, renderer ); final var processor = ProcessorFactory.create( context ); final var html = processor.apply( "$e^{i\\pi} + 1 = 0$" ); assertTrue( html.contains( " ) ); } When run, the unit test fails because the ProcessorFactory class does not create a TeX processor. Fortunately, the fix entails changing a single line of code. Open ProcessorFactory and change the line that creates an HtmlProcessor to the following: final var successor = new TeXProcessor( new HtmlProcessor( context ) ); It’s important that the TeX processing is applied after the Markdown document has been converted to HTML. To do otherwise would cause the syntax highlighter to fail when it adds style classes. Re-run the unit test to confirm that it passes. TeX Fonts Even though the unit test passes, we’re not quite finished because running the application—using the full version of Liberica JDK that bundles JavaFX—then typing in an equation shows: Download Full Liberica JDK Clearly, more work is needed because the fonts to render the formula are not being used. JMathTeX references the Computer Modern fonts; when the library generates an SVG document, the font names are embedded verbatim. Upon rendering the SVG element, WebView cannot resolve the font file name references. As such, we need to instruct the application to load the Computer Modern (cm) font file families when rendering the HTML document using WebView. We’ll load the fonts once because reading files into memory is a computationally expensive operation. First, though, we have to determine the font paths. The TrueType font files (e.g., cmex10.ttf) are inside the JMathTeX Java archive file. Recall from the first article that our Gradle build script will create an überjar: a combination of all the class files and resource files from every library required by the application to run. We can find the path to the font files from the command-line by invoking the jar command after building, as follows: ./gradlew clean build jar -tvf build/libs/mdtexfx.jar | grep ttf$ The output provides us with the font file paths, which we’ll load as resources into the application: 15396 Tue May 01 22:47:54 PDT 2007 be/ugent/caagt/jmathtex/cmex10.ttf 27120 Tue May 01 22:47:54 PDT 2007 be/ugent/caagt/jmathtex/cmmi10.ttf 21804 Tue May 01 22:47:54 PDT 2007 be/ugent/caagt/jmathtex/cmr10.ttf 22132 Tue May 01 22:47:54 PDT 2007 be/ugent/caagt/jmathtex/cmsy10.ttf Open HtmlPreview, then add the following methods: private String toFontFace( final String name ) { return format( "@font-face{font-family:'%s';src:url('%s') format('truetype');}", name, getResource( format( "/be/ugent/caagt/jmathtex/%s.ttf", name ) ) ); } private String getResource( final String path ) { return getClass().getResource( path ).toExternalForm(); } In effect, the above code generates font-face definitions that adhere to the W3C CSS specification, namely: @font-face{ font-family:'cmex10'; src:url('jar:file:.../mdtexfx.jar!/be/ugent/caagt/jmathtex/cmex10.ttf') format('truetype'); } After the line of code that binds the height property in the constructor, insert the following snippet to register the fonts: mView.getEngine() .setUserStyleSheetLocation( format( "data:,%s%s%s%s", toFontFace( "cmex10" ), toFontFace( "cmmi10" ), toFontFace( "cmr10" ), toFontFace( "cmsy10" ) ) ); The key step is using a Data URL to inject references to font files embedded inside a Java archive file. The Java archive file itself is referenced using the jar: protocol URI scheme. Rebuild, then restart the application. Here are a few different TeX expressions showing some of the TeX functionality that the JMathTeX library supports: We can now write TeX expressions in our editor. Download You may download the project; download and install the JMathTeX library separately. Summary This article introduced TeX and showed one way to integrate a format-agnostic TeX engine into our editor. We reviewed multiple Java-based TeX engines with specific requirements in mind, finally settling on the JMathTeX library. With help from the framework we developed previously, integrating the library meant creating a new processor to transform inline TeX expressions into SVG elements. The next article profiles the application to improve its performance. Related posts TeXnical Writing Part 1: Foundations TeXnical Writing Part 2: Markdown TeXnical Writing Part 3: Syntax TeXnical Writing Part 5: Performance - [DZone Refcard by BellSoft: Introduction to Cloud-Native Java](https://bell-sw.com/announcements/2021/04/02/Refcard-announcement/): Our mission is to bring value, technology, and knowledge to the Java™ community. Seeing how cloud-native solutions are spreading fast, we cannot stand aside. That’s why BellSoft has collaborated with DZone, a leading publisher of software development resources, to produce a short but action-packed intro to this exciting subject. DZone Refcard is a technical cheat-sheet format to cover a certain topic and guide through the first steps. While these tutorials are easy to read for beginners, they also include advanced details that mid-level or senior developers would find useful. In our Introduction to Cloud-Native Java, we’re going through the basic definitions and looking at the scope of capabilities brought by containers, JVM optimizations, multi-purpose frameworks, and native image technology. You’ll get everything that is required to make your project cloud-native! Here’s what the Refcard offers: the four pillars of the cloud-native methodology; the reasoning behind why Java is the optimal choice for your cloud-native project thanks to progressive features in OpenJDK; the three approaches to cloud-native Java with their benefits and drawbacks; a walkthrough example of how to set your own cloud-native Java environment, and much more! Click the button below to view and download Introduction to Cloud-Native Java (you should have a DZone account in order to download the Refcard). Continue reading on DZone - [Java in Docker on Apple Silicon](https://bell-sw.com/announcements/2021/04/09/Java-in-Docker-on-Apple-Silicon/): As you know Apple has begun the transition from Intel x86_64 processors to ARM64-based Apple silicon chips in Mac computers. Since then, the company launched several M1 processors: apart from plain M1 chip, the product line includes M1 Pro, M1 Max, and M1 Ultra, the last one being the most powerful so far with 20 CPU cores and 64 GPU cores. You can read more about M1 features in perks in our previous article dedicated to the topic. But there are still many jokes about some software that is lacking from the platform. The real challenge is to roll out programs that use low-level knowledge of the operating system and processor. Containerization and JVM are good examples of such technologies. They became friends not so long ago, and now we use the JDK in Docker as one of the main combinations of tools for everyday tasks. Many are worried whether or not they will be able to continue developing with the latest hardware. In my opinion, the answer is yes, but you need to check your toolbox. Let’s do this with a specific example. Moreover, we will not focus on the application code, but on what is needed for assembly and packaging. Table of Contents Installation Run application in a container Building a container image Cross-build for x86 Conclusion Installation So how would we build, run, and package some typical applications? For instance, I took the Spring Petclinic sample project. As you clone the sources it is really easy to build and run a jar file. As usual, I start with the Liberica JDK. No need for Rosetta 2 at this step, just download native JDK 11 assembly for macOS ARM 64 bit. I recommend installing .dmg, the binaries are notarized and the installer is simple and friendly. Then, download Maven or use Maven Wrapper (mvnw) to build the project, and run it as usual to check the build result: % java -jar target/spring-petclinic-2.4.2.jar The application will respond on http://localhost:8080 Note again, it is the macos-aarch64 JDK. Now, what about Docker? Docker is now compatible with Apple silicon, so there is no need for translator software apart from specific cases described on the developer’s website. What is important, it supports multi-platform images, so it is possible to build and run images for both x86 and ARM processors. Install Docker: Launch Docker Desktop, it will ask for privileges once to finish the setup. Now it is time to use the Terminal. Run application in a container Let’s get the JDK image! % docker pull bellsoft/liberica-openjdk-alpine-musl:11 It is Liberica JDK 11 for linux-aarch64. It downloads almost instantly, as the image is just 102 MB on disk (read how it was done in this article): If we check how the hardware is represented in the container, we will see that just as documentation says by default four of eight CPUs are exposed, they are (effectively) virtualized. The system inside is Alpine Linux, so standard Linux tools will show that: % docker run -it bellsoft/liberica-openjdk-alpine-musl:11 \ tail /proc/cpuinfo processor : 3 BogoMIPS : 48.00 Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 asimddp sha512 asimdfhm dit uscat ilrcpc flagm ssbs sb paca pacg dcpodp flagm2 frint CPU implementer : 0x00 CPU architecture: 8 CPU variant : 0x0 CPU part : 0x000 CPU revision : 0 The JDK inside is built for Linux with musl C library and aarch64 CPU, so it is easy to run our example in the container: % docker run -it -v $(pwd)/target:/target -p 8080:8080 \ bellsoft/liberica-openjdk-alpine-musl:11 java -jar \ /target/spring-petclinic-2.4.2.jar No new image is created here; the jar file just picked from the volume. And the app is again available at http://localhost:8080: Building a container image Of course it is possible to create a Docker image with the application. A simple Dockerfile might look like this: FROM bellsoft/liberica-openjdk-alpine-musl:11 WORKDIR / ARG JAR=spring-petclinic-2.4.2.jar ADD /target/$JAR app.jar EXPOSE 8080 CMD java -jar app.jar And the image is built as usual like % docker build -f ./Dockerfile -t my/petclinic-m1 . And run like % docker run -it my/petclinic-m1 Cross-build for x86 What’s next? For example, you can push this image and deploy it out of the box on AWS EC2 M6g instances in your fleet. They are also powered by Graviton 2 ARM64 CPUs, and the image is created on ARM64 for the same platform. But this image won’t run on x86_64 (:amd64) machines (“exec format error”)! Here we should just use a cross-build with the Buildx plugin: % docker buildx build --platform linux/amd64 \ -f ./Dockerfile -t my/petclinic-x86 . This new image is based on Alpine Linux and the Liberica JDK for x86, and can finally be deployed on x86_64 instances for testing or production. Conclusion A few more topics worth touching on are development, stability, and speed. IntelliJ IDEA with native support for M1 has been available since the end of 2020. It means that you can not only clone and build your application, but develop it as usual. The performance of native JVM is better than x86 emulation; you can read about this in a separate blogpost. Petclinic Spring Boot application, which was used as an example in this article, starts up pretty quickly (in 3-5 seconds) on M1 Macs when using system Java or Docker. Finally, cross-building can be done in the opposite direction, that is, container images for ARM-based systems can be prepared on x86 machines, and you can create multi-platform images as well. - [Liberica JDK 8u292, 11.0.11, 16.0.1 builds are released](https://bell-sw.com/announcements/2021/04/21/Liberica-8u292-11.0.11-16.0.1-are-released/): Today we announce the general availability of Liberica JDK versions 8u282, 11.0.11, and 16.0.1. The stable quarterly CPU cadence allows rolling out urgent updates and bug fixes exactly when they’re needed. The three April releases cover two (CVE-2021-2161, CVE-2021-2163) common security vulnerabilities and exposures with eight (four in each LTS version) backports. The release contains 501 fixes overall (127 in JDK 8, 271 in JDK 11, and 103 in JDK 16). Download Liberica JDK The BellSoft team has also included the following enhancements with all the GA builds: 1. Windows AArch64 support With this critical update of Liberica JDK, we introduce support for Windows AArch64 (ARM64) in the current 16.0.1 and LTS 11.0.11. This port is working with C1 & C2 JIT Compilers, and all garbage collectors: Serial, Parallel, G1, Shenandoah, ZGC. As for JFX, the newest releases support just Graphics and Controls modules for now, excluding Media and Webkit. These screenshots demonstrate Liberica’s availability for development on this platform. The first is Netbeans 12 (major Java-based IDE) on Windows AArch64 running Liberica 16: The second one features Recaf (written in JFX) on Windows AArch64 also running on the latest version of Liberica: Once again, we’d like to express appreciation to Microsoft for their first massive JEP 388 introduced last month in JDK 16. Thanks to this port and own developments, BellSoft engineers managed to add ARM64 support in OpenJFX — proving our commitment to the OpenJDK community and consistent contribution of all changes to the JDK mainline branch. View pull request by BellSoft 2. Static size optimizations in Liberica Lite GA Our January release introduced Early Access binaries for the Liberica Lite flavor, cut down in terms of static size. Now, these packages are becoming available for a wide audience. All have been reduced by 3–6 MB (which equals 5 to 14.7%). We at BellSoft believe that every little bit counts and each improvement makes working in the cloud or with Docker containers more efficient. 3. Dynamic size optimizations in Liberica Lite 11 Starting with this release, if your application runs with the G1 garbage collector, Liberica Lite 11 will return unused buffer memory to the operating system. Here we can show dynamic RSS size optimizations by running a Spring PetClinic sample project. You can observe an approximately 32% improvement in the figure below. 4. Regular Liberica NIK CPUs Liberica Native Image Kit is to receive critical patch updates on par with Liberica JDK. They should come out on a quarterly basis nearly the same time as BellSoft’s Java runtimes are released. In order to discover specific components or check version availability, please use the updated manuals for BellSoft Product Discovery API that now features a REST NIK API section. Check out Discovery API Upstream changes Cryptographic protocols are evolving along with any other software. According to monthly scans by SSL Pulse, more than 99% of the 150,000 most popular websites support TLS 1.2, which implies that 1% only support TLS 1.0 and/or 1.1. Since April 2021, they have been disabled in OpenJDK and Liberica JDK by default. In case your application or used frameworks rely on these weaker protocols, it’s really time you moved to more modern versions of TLS, well supported by the JDK. Versions 1.0 and 1.1 are officially considered outdated and potentially endangering your critical Java applications. We recommend switching to TLS 1.2 or 1.3 as soon as possible. Until you’re done with this migration, it is still possible to enable older protocols by removing “TLSv1” and/or “TLSv1.1” from the jdk.tls.disabledAlgorithms property in the java.security configuration file of the distribution. -jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, \ - EC keySize < 224, 3DES_EDE_CBC, anon, NULL +jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \ + DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL Being close-knit with the community helps us stay on top of the latest JDK trends. BellSoft is committed to delivering only the most useful features and contributing to your Java apps’ efficiency. Download all the new builds by clicking here or the button below. Download Liberica JDK - [TeXnical Writing Part 5: Performance](https://bell-sw.com/announcements/2021/04/23/TeXnical-Writing-Part-5-Performance/): Earlier in this series, we reviewed in brief the TeX typesetting system as well as the state of Java-based TeX engines. We settled on integrating JMathTeX into our application. In this final part, we’ll profile our application created with Liberica JDK to determine where optimizations will reward us with the most significant performance gains. Discover Liberica JDK Introduction Previously, we used libraries to translate TeX expressions into SVG elements. You may have noticed there was a delay between completing the expression and seeing it rendered. While not a problem for a document having a few TeX expressions, as the number of expressions increases, the preview will take longer to render. Eventually, we’d have to choose between delayed (asynchronous) output or unresponsive input. In this article, we’ll profile our application while it executes on Liberica JDK so as to understand where best to direct our efforts. This is in line with advice offered by Donald Knuth: "There is no doubt that the grail of efficiency leads to abuse. Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. [Good programmers] will not be lulled into complacency by such reasoning, [they] will be wise to look carefully at the critical code; but only after that code has been identified." ~ Donald E. Knuth, Structured Programming with go to Statements, ACM Computing Surveys, Vol 6, No. 4, Dec. 1974, p.268. We don’t know the code’s performance bottlenecks until we measure; spending a lot of time on micro-optimizations is usually wasteful. There’s a momentary blip during TeX equation rendering that we’d like to eliminate. In effect, we want to determine the “critical” path that Knuth described. We can accomplish this using a software tool—called a profiler—that gathers information about what method calls take up most of the application’s run time. Review Profilers Free and open-source tools to measure Java application performance include: BellSoft’s Liberica Mission Control NetBeans’ Profiler Oracle’s VisualVM Andrei Pangin’s async-profiler Richard Warburton’s Honest Profiler Another tool that can help with optimizing code is the Java Microbenchmark Harness (JMH), which is a solid piece of software geared towards building, running, and analyzing application performance down to the nanosecond if needed. We’ll use async-profiler because it can produce useful results in a few keystrokes. Install Profiler Install and configure the profiler as follows: Download the latest release for your target architecture. Extract the archive into $HOME/bin/async-profiler. On Linux, configure the kernel to reveal its call stacks as follows: Run ~/bin/async-profiler/profiler.sh to verify the installation. The profiler is ready. List Java Processes Java comes with a program called jps that lists the name and process identifier (pid) for all running Java applications. Here’s example output when run (your output will differ): $ jps 972229 Launcher 972067 GradleDaemon 639188 Main 978993 Jps The profiler can invoke the jps command to attach to the most recently launched Java process, automatically. Update Unit Test Writing a typical JUnit unit test annotated with @Test won’t give us a clean profile of the application’s performance for a few reasons. First, we don’t care to test the performance of JUnit itself. Second, if we ran the test using Gradle, we’d find that Gradle takes up much of processing time. Instead, we’ll update TeXProcessorTest such that we can run it from the command-line. We want to test the functionality for converting TeX to an SVG element with as little overhead as possible. Additionally, we should provide an equation that’s a bit more complicated than Euler’s identity, such as the Klein–Gordon equation: Open the TeXProcessorTest class then add the following method: static boolean test_Performance_TeXInput_SvgOutput() { final var processor = new TeXProcessor( null ); final var svg = processor.apply( """ $\\frac{1}{c^2}\\frac{\\partial^2}{\\partial t^2} \\psi-\\nabla^2\\psi+\\frac{m^2c^2}{\\hbar^2}\\psi=0$ """ ); return svg.startsWith( " ); } This is about as close to the conversion from TeX to SVG as we can get without making the toSvg method public in the TeXProcessor class. Next, introduce a main method that loops for a while so that we can profile our performance test: @SuppressWarnings( "StatementWithEmptyBody" ) public static void main( final String[] args ) { final long stop = nanoTime() + NANOSECONDS.convert( 5, MINUTES ); while( nanoTime() < stop && test_Performance_TeXInput_SvgOutput() ) { } } The additional static imports should resolve to: import static java.lang.System.nanoTime; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.NANOSECONDS; The unit test is updated and we’re ready to run the profiler. Run Unit Test Verify that the unit test can be run by executing the following commands: cd $HOME/dev/java/mdtexfx ./gradlew clean build test java -cp build/libs/mdtexfx.jar:build/classes/java/test \ com.mdtexfx.processors.tex.TeXProcessorTest Stop the unit test by pressing Control+c. Profile Unit Test Complete the following steps to profile our test harness: Open a new terminal to run the unit test and profiler at the same time. Re-run the Java command from the previous section to start converting TeX to SVG. Start the profiler as follows: ~/bin/async-profiler/profiler.sh -d 120 jps > profile.txt The -d 120command-line argument instructs the profiler to sample the application for 120 seconds; the jps argument tells the profiler to use the Java process command to find the pid of the most recently run Java application. Using > profile.txt directs the command’s output to a file named profile.txt, which contains the profiling results. Let’s look inside to begin our analysis. Analyze Results Open profile.txt then search for the word “percent” to find the list of application hot-spots. The list should begin similar to the following: ns percent samples top ---------- ------- ------- --- 14979296619 11.38% 1498 java.text.DecimalFormat.subformatNumber 12850404499 9.76% 1285 java_lang_Throwable::fill_in_stack_trace(...) 8279754791 6.29% 828 java.util.Arrays.copyOf Together, over 27% of the test is spent formatting numbers, filling stack traces, and copying arrays. Given that our test doesn’t purposefully throw any stack traces, that particular result seems strange. Array copies are likely string copies. Let’s start our sleuthing at the DecimalFormat class because that’s the biggest bottleneck. Investigate DecimalFormat Jump to the top of profile.txt then search for “DecimalFormat” to find one of its call stack invocations, which will look something like the following: --- 2679876934 ns (2.04%), 268 samples [ 0] java.text.DecimalFormat.subformatNumber [ 1] java.text.DecimalFormat.subformat [ 2] java.text.DecimalFormat.doubleSubformat [ 3] java.text.DecimalFormat.format [ 4] java.text.DecimalFormat.format [ 5] java.text.NumberFormat.format [ 6] org.jfree.graphics2d.svg.SVGGraphics2D.transformDP [ 7] org.jfree.graphics2d.svg.SVGGraphics2D.getSVGTransform [ 8] org.jfree.graphics2d.svg.SVGGraphics2D.drawString [ 9] be.ugent.caagt.jmathtex.CharBox.draw [10] be.ugent.caagt.jmathtex.HorizontalBox.draw [11] be.ugent.caagt.jmathtex.TeXIcon.paintIcon Return to IntelliJ IDEA, open the SVGGraphics2D class, then search for the transformDP method. Note that the following line invokes Java’s number formatter: return transformFormat.format(d); Control-click the transformFormat variable to navigate to its declaration: private DecimalFormat transformFormat; From the call stack, this confirms that every time part of a glyph is drawn, the transformDP method is performing an expensive operation. The DecimalFormat class is useful for converting floating point numbers to character strings when taking internationalization into consideration. Knowing that SVG is read and parsed by a machine, it’s computationally expensive to use Java’s built-in functionality for no practical benefit. It so happens that all Java-based SVG transformers suffer from this particular performance problem, meaning that a library swap won’t help much, if at all. What we need is a faster number conversion algorithm. In 2018, Ulf Adams published a paper titled Ryū: fast float-to-string conversion, along with benchmarks showing a 12-fold improvement over the JDK’s number formatter. Moreover, the source code was made available under the permissive Apache 2.0 License. While we’re looking at the SVGGraphics2D class, notice the code for getSVGElement around line 2731 creates multiple StringBuilder class instances. Similarly, the draw method starting at around line 1177 creates numerous StringBuilder class objects, one for nearly each call to getSVGTransform and getSVGPathData. Looking a little deeper, we can see that getSVGPathData is appending a lot of strings. This is likely the source of the third performance bottleneck identified by the profiler: copying arrays. For example, the following snippet is fairly representational of the string-copying code: b.append("Q ").append(geomDP(coords[0])) .append(" ").append(geomDP(coords[1])) .append(" ").append(geomDP(coords[2])) .append(" ").append(geomDP(coords[3])); A minor improvement would be to use a space character (' ') instead of a space string (" ") to avoid the overhead of checking string lengths. A bigger gain would be to pre-allocate a single internal string buffer, rather than continuously create new buffers. At the top of our offender list, though, is that the geomDP and transformDP methods must avoid formatting numbers using Java’s bundled DecimalFormat class. Our findings have revealed that the JFreeSVG library has room to be optimized. Furthermore, the library has many features that go beyond our needs by writing many irrelevant strings to the SVG document. Conceptually, we want to convert glyph paths from a font format to paths in an SVG format. We haven’t any ovals, polygons, fill patterns, gradient colours, or special namespaces to worry about, giving us further optimization possibilities. We’ll fix this issue after we look into the second bottleneck: filling stack traces. Investigate Stack Trace Fills Before we dig into the details, let’s revisit the purpose of an exception. According to the official Java documentation, an exception is: "an event, which occurs during the execution of a program, that disrupts the normal flow of the program’s instructions." That is, exceptions are meant to indicate a problem was encountered that wasn’t expected. When opening a file, for example, we anticipate that the file’s contents will be displayed. There are many ways that opening a file could fail: the file was a directory, its permissions were set to write-only, no more operating system file handles were available, it was deleted, the network connection to the file path dropped, there was data corruption, contents were an unknown file format, and so on. All of these are exceptions to the normal flow that prevent displaying the file contents. Let’s go back to profile.txt. Isolate the cause of the stack trace fills by searching for fill_in_stack_trace, starting from the top of the file. We find the following: --- 4000181209 ns (3.04%), 400 samples [ 0] java_lang_Throwable::fill_in_stack_trace(...) [ 1] java_lang_Throwable::fill_in_stack_trace(...) [ 2] JVM_FillInStackTrace [ 3] Java_java_lang_Throwable_fillInStackTrace [ 4] java.lang.Throwable.fillInStackTrace [ 5] java.lang.Throwable.fillInStackTrace [ 6] java.lang.Throwable. [ 7] java.lang.Exception. [ 8] java.lang.RuntimeException. [ 9] be.ugent.caagt.jmathtex.JMathTeXException. [10] be.ugent.caagt.jmathtex.SymbolNotFoundException. [11] be.ugent.caagt.jmathtex.SymbolAtom.get [12] be.ugent.caagt.jmathtex.TeXFormula.processEscape [13] be.ugent.caagt.jmathtex.TeXFormula.parse [14] be.ugent.caagt.jmathtex.TeXFormula. [15] be.ugent.caagt.jmathtex.TeXFormula. Judging from the call stack, it appears as though when processing an escape—a control sequence—if the symbol cannot be found then an exception is thrown. What makes a stack trace unusual is that we know all the symbols were found because the SVG rendered as expected. We’ll have to root around some more to understand the reason for the stack trace. Open the TeXFormula class and jump down to the processEscape method starting around line 534. Here we find some trivial issues: Using a default buffer size (new StringBuffer()) will cause array copies to be performed if parsing extends beyond 16 characters. Calling parseString.length() each iteration incurs a bitshift and conditional; it’s faster to store loop invariants—such as the string’s length—outside the loop. Neither of these are a silver bullet. When we take a step back, the algorithm for the processEscape method appears to be performing the following steps: If the parser encounters a backslash (\), process the escape command. Create a new (empty) buffer. Begin parsing the control sequence. If the next character is a valid control sequence character, append it to a buffer. Convert the mutable buffer to an immutable string. Look up the “atom” in a symbol table. If the “atom” cannot be found, see if it’s a predefined formula. Terminate when the end of the control sequence is found. We know the culprit is the SymbolNotFoundException class because of the profiler output. When we look at the code for SymbolAtom.get(String) we can see where the exception is being thrown: Object obj = symbols.get(name); if (obj == null) { throw new SymbolNotFoundException(name); } else { return (SymbolAtom)obj; } Now it’s a little clearer why the code is slow. For each character in a control sequence, if the control sequence does not yet map to a known value, an exception is thrown, which causes the JVM to fill in a complete stack trace. If the input expression has the control sequence \partial, for example, then twelve stack traces are filled: six for the unmatched atom and six for the unmatched predefined formula. In short, the main performance problem with the library is that exceptions are being used for normal control flow. Incomplete control sequences while parsing are to be expected and should not be treated as exceptional. To fix this, the parser could read the full control sequence before checking to see if it’s a known value (i.e., an atom or a predefined formula). Optimize The low-level technical details regarding how to implement these optimizations is not as important as the concepts and tools we’ve applied to find the bottlenecks in our application. To wit, I forked the JMathTeX library to include the aforementioned optimizations and brought in numerous font improvements courtesy of JLaTeXMath. Let’s swap out the original JMathTeX library with the forked version to see the performance difference. Replace TeX Engine First, complete the following steps to replace the TeX engine: Clone, build, then replace the engine. cd $HOME/dev/java rm Gradlepaths.app}}/libs/JMathTeX*.jar git clone https://github.com/DaveJarvis/JMathTeX jmathtex cd jmathtex ./gradlew clean build mv ./build/libs/jmathtex.jar ../mdtexfx/libs In the IDE, reload the changes to the build file. Open the TeXProcessor class. Remove import be.ugent.caagt.jmathtex.TeXFormula;. At this point IntelliJ IDEA will attempt to locate the new TeXFormula class. If errors continue to be unresolved, invalidate caches and restart the IDE, which may be found under the File menu. Replace the last lines of the toSvg method in the TeXProcessor class, starting from SVGGraphics2D with the following snippet (note that SVGGraphics2D is from JFreeSVG while SvgGraphics2D is from the JMathTeX fork): final var graphics = new SvgGraphics2D(); graphics.initialize( width, height ); icon.paintIcon( sComponent, graphics, 0, 0 ); return graphics.toString(); This swaps JFreeSVG’s SVG renderer for an implementation that uses the Ryū algorithm, among other performance improvements. The IDE should deduce the necessary imports, again, automatically. (Aside, we can remove org.jfree:jfreesvg from the build file.) Remember to remove the import for the SVGGraphics2D class to avoid a compiler error. Replace Fonts Next, we have to revise the font file name references. Start by opening the HtmlPreview class. There’s some duplication we can address in how the font faces are loaded. Replace the following lines: format( "data:,%s%s%s%s", toFontFace( "cmex10" ), toFontFace( "cmmi10" ), toFontFace( "cmr10" ), toFontFace( "cmsy10" ) ) ); with the following snippet that brings in the new font names: toDataUrl( "jlm_cmex10", "jlm_cmmi10", "jlm_cmr10", "jlm_cmsy10", "jlm_msam10", "jlm_msbm10" ) Then define the toDataUrl method as follows: private String toDataUrl( final String... names ) { return format( "data:," + "%s".repeat( names.length ), Arrays.stream( names ).map( this::toFontFace ).toArray() ); } This reduces the multiple toFontFace calls into a mapped function call (this::toFontFace) that is applied to all values provided by the names parameter. In so doing, we’ve abstracted away how a list of font names becomes a data: URL. Calling repeat means the computer will count the correct number of %s format specifiers on our behalf based on the number of font names given. The font file path has changed; update the toFontFace method by replacing the path in the format call as follows: format( "/fonts/cm/%s.ttf", name ) The fonts are updated. Re-run Profiler Re-build, then re-run the unit test and profiler as before. Open the profile.txt file to review where the CPU is spending most of its time: ns percent samples top ---------- ------- ------- --- 68679123646 50.44% 6868 ...RyuDouble.doubleToString 15170246750 11.14% 1517 java.util.Arrays.copyOf Previously, the number format call occupied about 11% of the processing time, but now it takes about 50%. The more time spent converting font glyph path data to vector graphic strings, the faster TeX expressions are rendered. Before the optimizations, 1 million TeX expressions took about 13 minutes to transform; after the optimizations, the time decreased to about 7 minutes, roughly twice as fast. Put another way, the software can now render around 2,000 short TeX expressions per second on a 3.3 GHz processor. For those who like a challenge, more optimization opportunities abound, including: Parallel computation. Although replacing TeX code in the Markdown document with its SVG equivalent mark-up must be performed sequentially, producing those SVG snippets from TeX code could be performed using multiple threads. Garbage collection. Even with high-performance garbage collectors, if an application generates many short-lived objects, responsiveness will diminish. All the tools mentioned previously can help pinpoint problems with memory churn. Reducing churn improves performance. Review Output Run the application to confirm the output. The first TeX expression may not appear instantly due to object initialization. After warming up, the application should be able to typeset the document without any noticeable delays, and no caching required. Here’s a screenshot of our application running on the full edition of Liberica JDK: We have accomplished our goal. Download Liberica JDK Related Resources Free and open-source resources related to these articles include: KeenWrite — My Java-based desktop text editor with live preview, variable interpolation, TeX expressions, spell checking, R integration, detachable tabs courtesy of TiwulFX Dock, and many more features. Typesetting Markdown — My blog series that describes a toolchain to produce beautiful PDF documents from Markdown source files by using Pandoc and ConTeXt. Together, these resources can help write documents using plain text while keeping the content separate from its final appearance. Download You may download the project’s final version. Summary Using a profiler provided us with insights into the slow parts of our application. After understanding where and why the code had sub-par performance, we optimized parts of the application that yielded the greatest improvement for the least amount of effort. Regardless of what profiler we use, the overall process remains the same: profile running software, analyze the results, trace the algorithm logic, and then think about how to mitigate the most severe bottlenecks. Thank you for reading! Related posts TeXnical Writing Part 1: Foundations TeXnical Writing Part 2: Markdown TeXnical Writing Part 3: Syntax TeXnical Writing Part 4: Math - [Choose your cloud-native fighter: JVM in containers, Quarkus or Spring Native](https://bell-sw.com/announcements/2021/05/20/choose-your-cloud-native-fighter-jvm-in-containers-quarkus-or-spring-native/): Do you have a ready-to-go Java app and want to take advantage of everything the Modern Cloud offers? Deployment is the next step, and you’ve come to the right place for guidance. Here we’ll focus on the three cloud-native development methods. They all are pretty efficient in boosting your software’s performance. The article follows our DZone Refcard but looks more into approaches themselves: be it pure containers or using a Java framework (Quarkus from Red Hat or Spring from VMware) for deploying native Java applications. Contents Preparation JVM in Linux containers MicroProfile & Quarkus Liberica NIK & Spring Native Our engineers are passionate about bringing enterprise projects to the cloud. We believe that’s the future. If you have a complex system full of moving parts and no time to learn the ropes, our expert is ready to help. Let’s meet and find out which cloud-native solution best suits your project. Start your digital transformation now! Book a meeting with a Java developer Preparation Before we start, let’s assume you’ve got pre-built software based on microservice architecture. If not, we suggest drawing some inspiration from our recent guide on how to build an e-commerce application for an online store. First, no matter which cloud service provider you choose, there needs to be a tool, or rather a platform, to manage containerized workloads and services in an automatic fashion, a so-called “container orchestrator.” We would recommend Kubernetes as it is open source, portable, and allows for scaling across multiple clouds. Not to mention it is a market leader: 59% of Java developers in large organizations reported using Kubernetes in production in 2020.1 Then comes an ingress controller necessary for providing your containerized application to the users. It also configures an HTTP load balancer and integrates proxies. Among many other options to choose from,2 you can discover that the Kubernetes project maintains an open source NGINX implementation, which would be your best pick. Voila, you’re all set for building microservice containers. There are multiple ways to create and deploy them in the cloud. We will talk about pure developers’ and DevOps processes here and bring to light three approaches: a rather straightforward one, a Quarkus-heavy one, and one centered around native images. JVM in Linux containers Cloud-native software development rests upon four pillars: microservices, containers, continuous integration/continuous delivery, and DevOps.3 Docker containers running in Linux virtual machines happen to be the easier approach to cloud-native Java. Having evolved from deploying WAR files on web servers to OS-level virtualization, this method is quite a potent optimization tool. You can have this setup for a Linux container as an example: a hypervisor host OS in the cloud, a guest OS on a VM serving as a host OS for Docker, Docker in the guest OS providing a container runtime, and JVM inside the container that runs Java bytecode. A suggestion from BellSoft: use Liberica JDK as your runtime component inside the container. It combines excellently with the majority of Linux distributions, including lightweight Alpine. The resulting build will help you decrease static and dynamic footprint, thus reducing expenses and increasing overall gains. Discover Liberica JDK One problem you might run across is container resource management. For instance, when the heap size is raised above the memory allowance of the container (enforced via cgroups), the application can be killed by the OS. Yet, this is not a thing to worry about: the mentioned issues are known to the OpenJDK community, addressed, and mostly solved. There’s a 1/100 chance you will think about how memory is allocated when deploying your Java application in a Linux container. And if you do, check out our previous article that goes deep into the subject. MicroProfile & Quarkus Another popular approach to cloud-nativity is using frameworks with MicroProfile support. These are aimed to optimize Jakarta EE for microservices and vary a lot. Some have their own APIs like Micronaut, some gather libraries for lightweight packaging like Dropwizard, and some originate from major JDK vendors like Helidon (Oracle) or Quarkus (Red Hat). We will elaborate a bit more on the latter, Quarkus specifically. The Quarkus project is a Kubernetes-native Java framework. It is essentially a full-stack set of technologies adapted to OpenJDK HotSpot for Java virtual machines and GraalVM for native compilation. For microservice creation, Quarkus works with Eclipse MicroProfile libraries along with such tools as Apache Kafka, Camel, dependency injection, Hibernate ORM (JPA and JTA annotations), RESTEasy (JAX-RS), Vert.x, Apache Camel, and more. Other bonuses include support for Maven and Gradle plugins (e.g., to run the Quarkus application in the development mode on a local or a remote machine) and relatively fast boot time. The Quarkus extensions built within the framework—or custom ones that you can develop on your own—make developing and deploying microservices easier. However, there are certain disadvantages to the MicroProfile approach: For cloud tasks, Quarkus relies heavily on Kubernetes features, such as for traffic management. It may not be convenient for companies that haven’t used Kubernetes since migrating to this tool requires time and commitment. Working with this technology at first might be full of trial and error, e.g., when enabling native compilation without the introduction of a Quarkus extension results in your Gradle task just failing without any useful messages. A simple reason: Quarkus and other frameworks have fewer libraries compared to our next contender, Spring Boot. Liberica NIK & Spring Native What if we take the JDK features, embedded into its code, and bring in a modern twist? Introducing Native Image: a truly progressive and cloud-native approach to Java. Here the optimal tool will be Liberica Native Image Kit (Liberica NIK), based on GraalVM CE with BellSoft’s contributions. It compiles Java bytecode into platform-dependent binary code to form fast and lightweight native executables. Discover Liberica NIK With the full-fledged support for Alpine Linux musl, Liberica NIK is your optimal choice to minimize resource consumption, achieve record startup times (up to 1/10 of a second), and save on memory. You may read more about its advantage in our Liberica NIK announcement. Every rose has its thorn, and this method is no different. There are certain things we cannot do with Native Image, which we described at length in a previous article about building microservices. In short, a different optimization model and a closed-world assumption might make Java apps behave unusually. So, be warned: not all software takes such kinds of optimizations. To overcome drawbacks, you should opt for the Spring Framework (which needs no introduction) and Spring Native. This experimental utility is used for transforming Spring apps into native executable files. Besides overcoming some of the native image limitations described above, it also provides a native deployment option for tiny containers. If you want to know more about Native Image and Spring Native, watch a JRush episode, where Josh Long and Andy Clement, Spring Native co-developers and experts, discuss the matter. Watch Jrush episode about Native Image Together, these tools constitute an advanced yet simple and rewarding method to building cloud-native Java applications. Now, which approach is the optimal one? This is for you to decide. Your business may go even further and build a customized microservices app based on Java SE or GraalVM. And if you need help, you can always rely on BellSoft professionals with 15+ years of Java experience. Book a free consultation This post is only part of our DZone Refcard Introduction to Cloud-Native Java. This extensive document contains basically everything to set up your first cloud-native environment. On the other hand, it features expertise on a specialized topic that is hard to find elsewhere. Click the button below to get access. Note that you should have a DZone account in order to download the Refcard. Continue reading on DZone References Why Large Organizations Trust Kubernetes Ingress Controllers for Kubernetes What are Cloud Native Applications? - [End of life for old TLS in OpenJDK and Liberica JDK](https://bell-sw.com/announcements/2021/06/03/end-of-life-for-old-tls-in-openjdk-and-liberica-jdk/): Note: This is a new version of this article, updated with the information on a vulnerability in OpenSSL 3.0, which is another example of why you need to keep your software up-to-date and protected. TLS 1.0 and 1.1 used to be a safety standard for a long time, but not anymore. The time to upgrade your security protocols is now long overdue. Let’s discuss the risks and benefits of switching to the modern version of TLS and if it is actually worth it. Table of Contents Deprecation of TLS 1.0 and 1.1 TLS vulnerabilities Critical vulnerabilities in OpenSSL 3.0 Were there any real successful attacks? What’s going to happen to my Java apps? What can I do? Bye, old TLS References Deprecation of TLS 1.0 and 1.1 The first steps to leaving the outdated TLS behind were taken quite some time ago. IETF deprecated old version usage in March of 2019. All popular browsers dropped the support for 1.0 and 1.1 in 2020, with only 1% of popular websites still working with these weaker protocols. As we already discussed, Liberica JDK release cycle is managed to ensure top-notch security, so in our April build, we disabled TLS 1.0 and 1.1 by default. This was executed by adding SSLv3, TLSv1, TLSv1.1 into the jdk.tls.disabledAlgorithms property of the java.security configuration file. jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, DES, MD5withRSA, \ BellSoft experts recommend keeping your online apps up-to-date to protect your clients and your business from attacks. Click the button below to allow our engineers to take care of your security. TLS vulnerabilities These are just some of the exploits1 proven to work with older versions of TLS. POODLE (“Padding Oracle On Downgraded Legacy Encryption”) originally took advantage only of the SSL 3.0 protocol, but in December of 2014, there was announced a new variant of the attack, exploiting flaws of CBC encryption mode in TLS up to 1.2. The “Man In The Middle” would force the client to employ the older SSL 3.0 protocol to communicate with the server and, as such, rely on the outdated cryptographic method — cipher block-chaining (CBC). At the time of writing, in May 2021, about 4% of 150.000 most popular sites still supported SSL 3.0 or lower and were vulnerable to attack2. And we can assume that this percentage is bound to increase if we take into account less popular (and likely less protected) servers. FREAK (“Factoring RSA Export Keys”) is another type of MITM (“Man In The Middle”) attack tricking the client and server into using outdated cipher suites that can be cracked in just a few hours with modern computing power. The attacker modifies the request from the client to enforce using the 40 or 56-bit encryption that used to be safe in the 90s but not anymore. BEAST (Browser Exploit Against SSL/TLS) was discovered as early as 2002, but the proof of concept was demonstrated only in 2011. This attack relies on injecting a Java applet into the victim’s machine and then sending crafted data to the target website over SSL. The attacker then sniffs the traffic to find the encrypted crafted data created by the applet and thus discovers the plaintext block. CRIME (Compression Ratio Info-leak Made Easy) exploits the compression algorithm in TLS utilized to reduce bandwidth. As repeated byte sequences are replaced by pointers to the first instance of the sequence, an attacker can inject different characters into the victim’s cookie and monitor the size of the response. This makes it possible to reconstruct the cookie value. Heartbleed is a vulnerability that exploits the heartbeat extension of OpenSSL. In TLS up to version 1.2, Heartbeat is a method to keep a connection alive by sending a message to the server to request a response containing the client’s data and its size. The trick is that an attacker can modify the size of the requested response, and the server will reply with large chunks of random data from its memory — including, for example, the private key of the server. Critical vulnerabilities in OpenSSL 3.0 The OpenSSL Project released a Security Advisory on November 1, 2022, concerning two critical vulnerabilities discovered in the OpenSSL library versions 3.0.0 to 3.0.6. Both were assigned a High severity level. Both can be triggered during a client or server's validation of an X.509 certificate. With the first one, X.509 Email Address 4-byte Buffer Overflow (CVE-2022-3602), a specifically crafted email address can overflow four attacker-controlled bytes on the stack. In the case of the second vulnerability, X.509 Email Address Variable Length Buffer Overflow (CVE-2022-3786), a buffer overflow can be caused by a malicious email address abusing an arbitrary number of bytes containing the “.” character (decimal 46) on the stack. The CVEs exploitation may lead to denial of service (DoS) or remote code execution (RCE). Both CVEs can be triggered if a vulnerable TLS client connects to a malicious server or a vulnerable TLS server requests client authentication and a malicious client connects. The CVEs were patched in version 3.0.7. If you are using OpenSSL 3.0, you should upgrade to the newest library version as soon as possible: this is the only way to deal with CVE-2022-3602. In the case of CVE-2022-3786, you can temporarily disable the verification of client certificates. Library versions 1.1.1 and 1.0.2 are not affected by the issue. If the library is bundled with the third-party software you are using, you should update the software as soon as the patch becomes available. This is also the case with operating systems with OpenSSL installed (Ubuntu 22.04, CentOS Stream 9, Alpine Edge, etc.). If you are using BellSoft’s containers based on Liberica JDK and these distributions, you don’t have to worry because our containers do not include this library by default. Also, Amazon Linux 1 and Amazon Linux 2 don’t ship with OpenSSL 3.0, so no patch is required. As far as Alpine Linux is concerned, the patch is already available. The OpenSSL package for Alpaquita Linux has also been updated, the patched version can be found in Alpaquita’s repositories. OpenJDK distributions, including Liberica JDK, use their own implementation of TLS and therefore are not affected. Were there any real successful attacks? Sure there were. A list of organizations falling victim to these exploits includes Canadian tax agency3, UK parenting site4, the large USA hospital chain5 and even Yahoo mail6 — and these are just some cases that were actually discovered. Nobody really knows how many successful attacks went unnoticed. While there are no public cases of Java™-affected attacks, big companies take these risks very seriously. For example, in 2011, Mozilla was about to ban Java™ from Firefox right after the BEAST vulnerability was demonstrated, although Oracle resolved the issue with a promptly released patch7. The risk of new exploits suddenly popping up is always present within the systems that are not up-to-date. What’s going to happen to my Java apps? Probably nothing, if you update regularly. It is likely you are already using the TLS 1.2 protocol. And even if you keep older TLS on client and server applets in your LAN or intranet, they will still be able to handshake and exchange information. But maybe you work with people that have never updated their web browser for the last ten years. Or you like to keep your client apps up-to-date but still rely on the good-old server program written in 1999. And we get it — “If it ain’t broke, don’t fix it.” However, this time the ancient code may stop working. What can I do? There are a few ways to solve the potential problems. You may update and then enable back the TLS 1.0 and 1.1 support in your runtime configuration by removing them from the jdk.tls.disabledAlgorithms property of the java.security configuration file. Be aware, that also means you accept the risk of dealing with security vulnerabilities of older TLS protocols described earlier, so think hard if it is worth it. You will just postpone the issue you will need to deal with in the future. If you are absolutely not going to update, you can still manually disable TLS 1.0 and 1.1 on your server. In your configuration files, find the lines referring to protocols (for example, server.ssl.enabled-protocols=TLSv1.1, TLSv1.2 in Apache Tomcat) and modify them to keep only the safe versions of TLS. Don’t forget to test your server with the OpenSSL toolkit. Run the following commands: openssl s_client -connect localhost:443 -tls1 and openssl s_client -connect localhost:443 -tls1_1 should be rejected. openssl s_client -connect localhost:443 -tls1_2 and openssl s_client -connect localhost:443 -tls1_3 should be accepted and print the certificate’s details. The safest solution is to update all your apps on the server and the client sides to be compatible with newer TLS. After all, we upgrade security for a reason. This will take more time and effort, but it’s a comprehensive solution in the long run. Bye, old TLS So, should you work in legacy mode, or is it time to rebuild your apps following modern safety standards? The choice is yours, but the April update of OpenJDK and Liberica JDK marks one more step to deprecating the older TLS security protocol. Get the latest and safest version of Liberica JDK! Discover Liberica JDK References Examples of TLS vulnerabilities and attacks SSL Pulse Heartbleed bug exploited to steal taxpayer data Heartbleed hacks hit Mumsnet and Canada’s tax agency Report: Devastating Heartbleed Flaw Was Used in Hospital Hack Critical crypto bug exposes Yahoo Mail Attack against TLS-protected communications - [JDK Flight Recorder vs. Stop the World Pauses](https://bell-sw.com/announcements/2021/06/09/jdk-flight-recorder-vs-stop-the-world-pauses/): This post will focus on Stop-the-World pauses in the JVM and how JDK Flight Recorder (JFR) can help you identify the root cause. I’ve touched on this topic before in Hunting down memory issues…, but now the goal is to get a deeper insight into its aspects. Contents Stop-the-World in HotSpot JVM Safepoints in JDK Flight Recorder Entering the safepoint Why may it take a long time to enter a safepoint? Thrashing Memory-mapped I/O Long internals between safepoint checks in JIT-compiled code Monitoring safepoints without JFR Conclusion JFR is indeed a hidden gem of OpenJDK. This powerful tool is integrated in Liberica JDK 11 and newer. You can check it out and discover its use for your projects like I did for mine. Download Liberica JDK with JFR Stop-the-World in HotSpot JVM Before moving on to details, I need to make some clarifications on terminology. The Stop-the-World (STW) pause is a general term for a state of the JVM when all application threads are suspended for the duration of a specific internal JVM operation. In the HotSpot JVM used in OpenJDK, there is a more specific term: safepoint. Citing the HotSpot Glossary, a safepoint is defined as: “A point during program execution at which all GC roots are known and all heap object contents are consistent. From a global point of view, all threads must block at a safepoint before the GC can run. (As a special case, threads running JNI code can continue to run, because they use only handles. During a safepoint they must block instead of loading the contents of the handle.) From a local point of view, a safepoint is a distinguished point in a block of code where the executing thread may block for the GC. Most call sites qualify as safepoints. There are strong invariants which hold true at every safepoint, which may be disregarded at non-safepoints. Both compiled Java code and C/C++ code be optimized between safepoints, but less so across safepoints. The JIT compiler emits a GC map at each safepoint. C/C++ code in the VM uses stylized macro-based conventions (e.g., TRAPS) to mark potential safepoints.” Safepoints are utilized not only for GC but also for other JVM operations. Below are key categories of JVM operations causing safepoins: Garbage collector related; JIT compiler related; Bias locking related; Diagnostic related (e.g., thread dump, heap dump, profiling, and debugging). Besides explicit VM operations, certain cleanup tasks related to internal JVM housekeeping are delayed until the next safepoint. As there is no guarantee that a VM operation will run regularly, safepoint could be initiated without any explicit VM operation. Effective pause time associated with a safepoint can be split into three components: Entering safepoint time — the JVM needs all application threads to arrive at a safepoint, which does not happen instantly. This period is unrelated to the type of operation to be executed at a safepoint; Executing tasks pending for the next safepoint; VM operation time — operations obviously need some time to complete. This component is operation dependent. Safepoints in JDK Flight Recorder JDK flight recorder has multiple events related to safepoints, yet some are disabled by default. You can enable a safepoint-specific event in the “Start Flight Recording” dialog. Switch to low-level configuration mode, type “safepoint” in the filter, and check “Enabled” next to all options. If you have never started Flight Recorder before, detailed instructions are provided in my first post on JFR. Once a recording is done, you can find the following views with information related to safepoints. The standard report available in Mission Control is “VM Operations” under the “JVM Internals” category. This report presents a summary of VM operations grouped by type. It is also possible to see individual events in the “Event log” tab. Keep in mind that event durations in this report are just durations of VM operations and do not reflect the duration of the Stop-the-World pause. Other safepoint-related events don’t have predefined reports in Mission Control so far. We will use “Event Browser” to view them. You can enter “safepoint” into the filter on top of the event type tree to see relevant events. All event types in the screenshot below are disabled by default, so you need to enable them explicitly before starting Flight Recorder. The “Safepoint Begin” event is the one stirring the most interest: its duration is the total duration of the Stop-the-World pause. Information about safepoint is scattered among multiple events. Regardless, different events related to the same safepoint could be correlated by the “Safepoint Identifier” column. We can use custom filters to make digging through safepoint details more ergonomic. Right-click on the “Event Browser” node at the “Outline” side view on the left and choose “New Page” > “Custom Page …”, then select the “Safepoint Begin” event type to create a new custom report. A new node will appear in the “Outline” side view; select it. You will see a report showing only “Safepoint Begin” events. Now, right-click and delete the existing filter node in the upper section of the report. Instead of filtering events by type, we will sieve all events with the “Safepoint Identifier” attribute. Right-click and choose “Add Filter from Attribute” > “Safepoint Identifier.” Lastly, change the type of the created filter node from “==” to “exists”. The event log (section at the bottom) contains all safepoint-related events, including the “VM Operation” event. Now sort by “Start Time,” and all events associated with the same safepoint will be naturally grouped. The diagram below shows relations between different JFR events affiliated with a safepoint. Normally the VM operation is the main factor defining the duration of the STW pause. Unfortunately, though, sometimes phases before the operation turn out to be problematic. Entering the safepoint Entering the safepoint is a fairly sophisticated process. The JVM needs to stop every Java thread at the point where all local variables are predictably stored at stack (many VM operations need to traverse stack for heap references). A cooperative safepoint protocol is used to coordinate running threads to enter a safepoint state. Generally, each Java thread could be in one of the following conditions: A thread is blocked with API in the runtime library (it’s not in the RUNNABLE state). In this case, no additional actions are required; A thread is RUNNABLE and is running in the interpreter. In this case, the thread could be suspended on the next bytecode operation. The JVM replaces the interpreter jump table with special jump table variants where each byte code index is mapped to enter a safepoint routine; A thread is RUNNABLE and is running JIT-compiled code. This is most tricky; the compiler could do a lot of operation reordering and load/store optimizations. The compiler has to emit special safepoint checks. The thread will suspend itself once it reaches the next checkpoint after the safepoint protocol is initiated by the JVM (I will get back to this topic later in the article); A thread is RUNNABLE and is running native code via JNI. Such threads don’t need to be stopped, and they may continue to run during the JVM Stop-the-World pause. However, if this kind of a thread returns to calling Java method, calls Java method, or tries to access data in Java heap via JNI API, it will be blocked until the safepoint ends; A thread is RUNNABLE and is running JVM intrinsic code or the “JavaCritical” JNI method. Such thread cannot be stopped until it will complete the current operation and enter the JIT-compiled code section. The key thing is that all application threads need to be stopped. Until each and every thread enters a safepoint, the operation cannot start. Or put it another way—even if just one thread is stuck and isn’t able to enter for some time, it will effectively freeze the JVM. Why may it take a long time to enter a safepoint? All Java threads in the RUNNABLE state should enter a safepoint. Although RUNNABLE, in JVM terms, doesn’t necessarily mean that a thread is actually running on CPU at the moment. The thread being RUNNABLE in Java could be: Actually running on a CPU; Scheduled to run by the OS but waiting for a CPU slice; Blocked due to blocking the OS call; Blocked due to a page fault. Case 1 is the normal condition. The thread can proceed with the safepoint protocol. Case 2 is possible if the host is starved of CPU resources. In Linux, it’s hard to monitor how long threads spend in the scheduler queue. Therefore, we can rely only on indirect metrics like total host CPU usage for post mortem diagnostics for such conditions. Remember that CPU starvation means overall degradation of performance, not just slowed-down VM operations. Case 3 means the thread is blocked on an explicit OS call, which will be wrapped via JNI so that such thread will be exempt from the safepoint protocol. Case 4 is a different story. Unlike Case 3, a page fault may block execution at any instruction. The JVM cannot add any safety net for these cases. The thread has to be resumed by the kernel and continue execution to, first, finish the safepoint protocol and then let it proceed with the VM operation. It is obvious that Case 4 is a most problematic condition here, having the potential to cause abnormally long safepoint time. What are common reasons for page faults in Java applications? Thrashing System-wide swapping (aka thrashing) could be one reason. As a rule of thumb, the JVM should keep its heap and a few other vital data structures resident in memory. If JVM’s memory starts to be paged out, it hits app performance quite badly. The GC also does not like memory being swapped out, so this condition has the double impact of the STW pauses. Swapping, in turn, has two causes: Host memory shortage — physical memory is not enough for all processes running on the host. Container resident memory limit — it is possible to configure resident memory usage limit per container. This limit may lead to thrashing for a specific container even if the host has enough free memory (my previous post provides more details for this situation). Memory-mapped I/O Java runtime library has an API for memory-mapped disk I/O. “Memory-mapped” means that reading or writing to a file on disk are regular memory operations in application code. The actual content is loaded from disk to memory page on first access and eventually synchronized back to disk if modified. The kernel is responsible for moving data. It’s worth pointing out that for memory-mapped I/O moderate page fault rate is expected. Just one thread busy with memory-mapped I/O is enough to impact the safepoint for the JVM globally. Ironically, a memory mapping approach should reduce I/O overhead by eliminating OS calls, but in the JVM case, it can introduce worse problems interfering with the safepoint protocol. I would advise avoiding memory-mapped I/O for services that are response time critical and process requests in parallel. Often libraries (e.g., Lucene) support both memory-mapped and non-memory-mapped I/O. You really need to assess the impact of the JVM STW pause before enabling the memory-mapped options in your middleware. Long intervals between safepoint checks in JIT-compiled code Above, I have described situations where Java threads are prevented from running by the OS. However, it’s possible for threads to run on CPU without yet completing the safepoint protocol in a timely manner. The issue could be with the code produced by the JIT compiler. It should place checks for safepoint regularly in the emitted code. Each method call site is typically eligible for a safepoint check (a method should always ensure its local variable is appropriately placed on the stack before calling another method). Loop back branch is another natural point for a safepoint check, even though a safepoint implies a barrier in operation reordering, thus forcing many optimizations. A check for the safepoint may be omitted for “counted” loops (loops with an index variable of type int). The compiler takes risk for potentially long periods between checks in runtime and better general execution performance. Besides the “counted” loop, JVM intrinsics can also have high upper bounds for execution time. For instance, when you create a huge array instance, array memory is zeroed at the moment of allocation with no safepoint possible until it’s done. Fortunately, such situations are rare enough. The JIT compiler is well balanced in the JVM, and chances to hit this problem class is close to zero in typical Java code. In my practice, there were just a handful of situations where I witnessed such a problem: Operations on unreasonably long strings; Extensive Java BigNumber manipulation; Handmade statistics library working on long arrays of samples. The problem has a further unpleasant side: safepoints are occurring fairly frequently in the JVM with a possibility of each becoming an unreasonably long STW pause. The good news is that code profiling will typically be a highly problematic section of code as hot thanks to the safepoint bias. Monitoring safepoints without JFR JDK Flight Recorder is not the only tool to monitor safepoints. The JVM has multiple options to enable logging safepoint-related information to the console. -XX:+PrintGCApplicationStoppedTime — contrary to its name, this option will let the JVM log all STW pauses, related or unrelated to garbage collection, to GC logs. If enabled, you’ll see lines like this: Total time for which application threads were stopped: 0.0007555 seconds, Stopping threads took: 0.0005893 seconds The message above does not tell you much about the nature of the safepoint, though. -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 — such a combination of flags will let the JVM print more details after each safepoint. The output will look as follows: vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count 9.220: ThreadDump [ 17 8 8 ] [ 1 19 21 0 0 ] 8 This output contains much more details, including VM operation, thread counts, and time (in milliseconds) spent in the pre-safepoint phases. It also contains the number of page traps during safepoint, which is very handy to identify I/O-related issues. A downside here is formatting, weird and hard to parse. The idea behind this option is to log a summary of passing safepoints in the table form. JDK 11 made available new structures logging system, and safepoint logging could be enabled with the following option: -Xlog:safepoint*=debug The result will look like this: [13.173s][debug][safepoint ] Safepoint synchronization initiated. (18 threads) [13.173s][info ][safepoint ] Application time: 2.0725501 seconds [13.173s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.173s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.173s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.173s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.173s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.173s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.173s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.209s][debug][safepoint ] ... found polling page loop exception at pc = 0x0000000012975dbc, stub =0x000000000aed7100 [13.209s][debug][safepoint ] Waiting for 8 thread(s) to block [13.210s][info ][safepoint ] Entering safepoint region: ThreadDump [13.210s][info ][safepoint,cleanup] deflating idle monitors, 0.0000004 secs [13.210s][info ][safepoint,cleanup] compilation policy safepoint handler, 0.0000004 secs [13.210s][info ][safepoint,cleanup] updating inline caches, 0.0000004 secs [13.210s][info ][safepoint,cleanup] resizing system dictionaries, 0.0000004 secs [13.210s][info ][safepoint,cleanup] purging class loader data graph, 0.0000000 secs [13.210s][info ][safepoint,cleanup] safepoint cleanup tasks, 0.0000809 secs [13.210s][info ][safepoint ] Leaving safepoint region [13.210s][info ][safepoint ] Total time for which application threads were stopped: 0.0375086 seconds, Stopping threads took: 0.0372714 seconds Conclusion Stop-the-World (aka safepoints) pauses are fairly frequent in the JVM, and not all of them are related to garbage collection. Normally non-GC safepoints have submillisecond duration and go unnoticed. And still, there are conditions that cause abnormal STW pauses due to a long time to safepoint. Built-in JDK Flight Recorder profiles do not have detailed safepoint-related events disabled. If you suspect a non-GC-related STW pause, enabling this event in Flight Recorder is the first step in your investigation. Besides metrics collected by JFR, I also recommend looking at OS metrics to identify situations such as thrashing and CPU starvation. - [Efficient code vs string concatenation. Solve This Java™ Snippet!](https://bell-sw.com/announcements/2021/06/25/efficient-code-vs-string-concatenation-solve-this-java-snippet/): Let's have some fun with Java code! Here is a code snippet: int n = 100; String s = "1"; for(int i = 0; i < n; i++) s = s + 1; A cycle that repeats an ‘n’ number of times and adds ‘1’ to an ‘s’ string with an assigned value of ‘1.’ Can you assess the algorithmic complexity? This may not be as simple as it seems. It may look like doing ‘n’ operations means O(n) complexity. Well, study the operation precisely. The thing you should notice is that it’s a concatenation of a string accumulator and a string representation of an integer. At each iteration of the loop, ‘s’ grows: 1 11 111 .... Since Java strings are immutable, growing means copying all previous contents. The number of copied characters will be (1) + (1 + 1) + (1 + 1 + 1) + … , which is a well-known sum of an arithmetic progression calculated as (n^2-n)/2. It means time complexity will be quadratic: O(n^2). Let’s check what happens under the hood and how the benchmarks behave. This explanation is relevant for Java versions newer than 8, where such code is more efficient. There are comprehensive materials online that explain the Java 8 situation in depth.1 All examples are made with Liberica JDK. Discover Liberica JDK If we inspect the bytecode targeted to Java 9+, there will be fragments like the invokedynamic call for string concatenation: 14: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; and BootstrapMethods: 0: #38 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; But basically, the default behavior is similar to what see for Java 8 target when javac (the primary Java compiler) generates concatenation code equivalent to s = new StringBuilder().append(s).append(1).toString(); What does interest us the most in this example? The JVM performance of this code, and whether it’s good enough. Now we’ll run some tests. A naive JMH benchmark: @State(Scope.Thread) public class ConcatBench { @Param({"100", "400"}) int n; @Benchmark public String chain() { String s = "1"; for(int i = 0; i < n; i++) s = s + 1; return s; } @Benchmark public String sameSB() { String s = "1"; StringBuilder sb = new StringBuilder(s); for(int i = 0; i < n; i++) sb.append(1); return sb.toString(); } @Benchmark public String newSB() { String s = "1"; for(int i = 0; i < n; i++) { s = new StringBuilder().append(s).append(1).toString(); } return s; } } This example also has a third code variant where StringBuilder is created outside of the loop. If we target javac to Java 16 and run the benchmark on JDK 16, we get the following: Benchmark (n) Score Error Units ConcatBench.chain 100 940733.514 ± 39773.301 ops/s ConcatBench.newSB 100 867987.506 ± 10770.221 ops/s ConcatBench.sameSB 100 4937177.165 ± 166843.186 ops/s # 5.2x faster than chain ConcatBench.chain 400 128595.073 ± 819.279 ops/s # 7.3x slower than n=100 ConcatBench.newSB 400 127075.294 ± 1383.764 ops/s ConcatBench.sameSB 400 1403760.812 ± 37459.769 ops/s # 3.5x slower than n=100, 11x faster than chain When ‘n’ grows by four times, the code variant where StringBuilder is reused slows down by 3.5x (close to linear), while the variant with re-created StringBuilder slows down by 7.3x. The exact quadratic slowdown would be 16-fold. However, there are additive costs and optimizations for data copying (see StubRoutines::jbyte_disjoint_arraycopy in the OpenJDK code). As an experiment, you can run the benchmarks on your hardware. Try playing with javac and JDKs and compare 7, 8, 11, and 16 bytecode running on JDK 7, 8, 11, and 16. Download Liberica JDK In short, prepare for a higher cost The complexity will be quadratic — expressed as a sum of an arithmetic sequence to n terms — and it will be in the same proportion for both the number of operations and memory. The reason is string concatenation, an infamous Java caveat. Isn’t performance in the JVM exciting? Would you like to see more of such posts? Subscribe to our newsletter and get informed about new articles first! Subscribe to our newsletter References and useful links java.lang.String Catechism - Stay Awhile And Listen by Aleksey Shipilёv String concatenation with Java 8 GitHub: jdk/stringopts.cpp StringConcatFactory#makeConcatWithConstants() src/hotspot/share/opto/stringopts.cpp - [License to support. The service your Java apps deserve](https://bell-sw.com/announcements/2021/07/08/license-to-support-the-service-your-java-apps-deserve/): BellSoft cares about our clients’ success, first and foremost. Successful people know that you need to constantly improve yourself to stay the best. And to do so it is important to build a support system consisting of your colleagues, friends and family. And in that way, people are not unlike the Java applications. To stay on top they require updating, enhancing and staying safe. And reliable support is a critical factor in letting that happen. In this article, we will discuss the value of JDK support for your software development projects, find out how it helps in enhancing your apps, discover some cases where it was crucial, and present the principles that BellSoft utilizes in building the support system for our main product — Liberica JDK. Download Liberica JDK Summary But before that, we would like to start with finding the answer to what we call “the most important question” related to the well-being of your apps. Contents The most important question about your app The support system for your Java applications How (and why) the reliable support works Case 1. Mysterious crashes Case 2. How to fix the 20-year old bug Case 3. Overheating the Pi BellSoft is here to support you The most important question about your app There are many aspects in programming requiring your attention, but all of them should be based on one simple question: Does my application actually work the way I want it to? And although the first response that comes to mind is usually “Of course it does,” it is not always honest. As we already discussed, to stay successful, the program has to be constantly improved and enhanced. Are you sure this is what happens to your product? Software development issues worth considering If you can honestly answer “yes” to these questions — congratulations, that is a great achievement, and it’s likely that you have a team dedicated to Java runtime support! But if you feel that you should focus on improvement more, then we have a solution. Just like successful people, your amazing apps need a support system from a reliable source (that is BellSoft with its engineers) to function. The support system for your Java applications It is no mystery that building a support system is one of the most effective ways for true leaders to stay on the top. Statistically, successful people usually enjoy support from many sources: family, coworkers, friends. And similarly, your apps require some help from your team, and for them to receive it, some workload needs to be taken off from your developers and technical support engineers. Good support is supposed to take care of the runtime issues, keep your Java app stable and protected, and let your people work on implementing new features. But that does not always happen, as most of the developers are not satisfied with the level of technical support they receive. Developers’ responses to unqualified technical support So there is a cry for help from developers. And the answer we found in BellSoft is building a JDK support system based on these principles: “Safety first.” We understand that your business needs to be protected. We know the value of security; “Always ready.” You don’t have to wait for days to get your answer. We are always there to help you as fast as possible; “Skip the middleman.” The people who work on our product are the same people who help you with it; “Solve for one — solve for everyone.” Every reported bug or issue we solve is fixed in the next update so that you won’t encounter it; “Pay for what you need.” We don’t think that good support should be unaffordable. We offer many support plans and customize them according to your needs. We also make an effort to make it clear what you are paying for and how it helps you immediately and in the long run; “We create Java™ to help you.” Our developers are members of the OpenJDK Vulnerability Group and Java Community Process Executive Committee, so we use our experience to make Java™ better for everyone; How (and why) the reliable support works These are the two most dangerous problems that can affect the runtime environment: Your apps don’t function well They are becoming slow, crash, or just refuse to start up. That means losing uptime, then money, resources, and ultimately clients. Your apps are not secure There are many ways to steal or manipulate the information they use, even if you work behind the firewall, and new ways of doing it appear constantly. Enjoying our support means leaving all these issues to us to solve promptly. Discover BellSoft support The reliable support can help you with Being sure you work in the certified, proven to be safe runtime environment with all the latest security vulnerabilities patched. Choosing an obscure Java runtime or some “work-in-progress build” can lead to unexpected faulty updates that crash everything;1 Freeing the time of your developers to enhance your app and add the new features; Removing the need to hire the costly talent with a particular set of security skills; Not worrying about security and keeping a stable runtime environment to run your apps. Here are just a few cases where our engineers solved client’s problems: Case 1. Mysterious crashes Client: The cloud computing and virtualization technology company. Description: Our client developed the new project that kept crashing on any version of OpenJDK. Solution: In a very short time we made the fix to the runtime code and it worked from the first try. We made a hotfix for the client and added the solution to the next security patch. Case 2. How to fix the 20-year old bug Client: QZ, Developer of printing API. Description: Print quality was severely degraded. Solution: We started working on the issue the day it was submitted. It turned out to be a multi-facet problem that arose from several bugs, including one that was over 20 years old. We also had to communicate with a third-party open source project developers to change its public API. As the issue was print quality related, it was paramount for our client’s product. It made QZ abandon the previous JDK support provider they had and turn to BellSoft for fixing the problem. In case that solution did not work, the client would have been ready to abandon the ecosystem for good. “I wish I had discovered BellSoft 6 years ago, we had hit too many dead-ends with other providers. We were prepared to leave Java altogether. With BellSoft, not only is the response time the best we’ve had, but the knowledge and perseverance are the defining difference for our company. In our case, an age-old Java bug was patched but it uncovered more issues, which BellSoft promptly patched as well. This was no small feat; BellSoft asked a downstream project to permanently change its public API, which they did. This level of follow-through and professionalism is a trait we share with BellSoft and is the key to a successful product.” Tres Finocchiaro,Lead Programmer, QZ Tray desktop software Case 3. Overheating the Pi Client: The OpenJDK community. Description: There is a Raspberry Pi ARM port of OpenJDK (mostly written by people who work at BellSoft). At some point after many updates the runtime started to overload the processor making running any application impossible.2 Solution: Our engineers created a fix that solved the issue and promptly submitted it. Just another example of BellSoft free work for the developer community. BellSoft is here to support you! If any of the issues we listed sound familiar, do not fret! We welcome you to join our support system, choose the plan that suits your needs and experience the new life of your apps, where they grow, get better and receive new features constantly. After all, as the wise man said: “If you don’t have the support of others you cannot achieve anything altogether on your own.” Get advice on Java support References Mystery meat OpenJDK builds strike again Raspberry Pi bug BellSoft engineers patched in OpenJDK - [Liberica JDK 8u302, 11.0.12, and 16.0.2 builds are out](https://bell-sw.com/announcements/2021/07/22/liberica-8u302-11-0-12-and-16-0-2-are-generally-available/): The quarterly release cadence is the best thing that happened to Java™. Once again, it proves its effectiveness, as the three new CPUs of Liberica JDK — 8u302, 11.0.12, and 16.0.2 — eliminate an outstanding amount of issues from the previous releases. Here’s a quick July release overview: 4 security issues fixed: CVE-2021-2388 (CVSS score 7.5), CVE-2021-2369 (CVSS score 4.3), CVE-2021-2432 (CVSS score 3.7), CVE-2021-2341 (CVSS score 3.1); 16 total security fixes; 501 backports and bugs fixed: in Liberica 8u302: 145, in Liberica 11.0.12: 281, in Liberica 16.0.2: 72 + 3 in JFX (2 security fixes and 1 bugfix). Download Liberica JDK BellSoft engineers are always on the lookout for what our customers need in their projects. Apart from improving the security and performance of our JDK/JRE, today we announce the following new features. 1. Liberica NIK Core By popular demand, we release a special flavor of the Liberica Native Image Kit compiler: Liberica NIK Core. This package contains Java™ code only (meaning builds of Liberica VM and open source GraalVM Community Edition) with no additional language plugins. Download Liberica NIK Core Starting from July 2021, you have two options to convert bytecode into extra-performant native executables and accelerate applications. Liberica NIK Standard remains the best for polyglot JVM-based projects. As for Liberica NIK Core, we consider it optimal for pure Java development — especially with Spring Native1 and the whole Spring ecosystem. It is available for the same range of platforms as the Standard. See the full list of architectures and operating systems on the Supported Configuration page. 2. JDK 8 Lite optimized for size At long last! Liberica JDK 8u302 is released in the Lite form, just like JDK 11 and 16, further extending the binary package offering. Thanks to this improvement, we also managed to trim down Docker containers. Compare the binaries for different platforms using the table below. See how much you can save by switching to Lite from Standard. JDK Standard JDK Lite Alpine (apk) 97.54 48.03 Alpine (tar.gz) 99.90 49.27 Debian (deb) 83.14 35.89 CentOS (rpm) 88.64 40.47 Linux (tar.gz) 101.65 51.32 JDK 8u302 Standard and Lite binary packages sizes, Mb The Liberica JDK Alpine musl image continues to be the smallest on the market and produce Java microcontainers. Looking for a way to minimize valuable cloud resources or migrate to microservices? The Lite version of Liberica JDK 8 is worth checking out. Download Liberica JDK 8 Lite 3. Smaller containers with JDK 11 and 16 Lite This goes without saying: in software development, a reduced size is a good size. Liberica 11.0.12 and 16.0.2 Lite received several optimizations, which has made Docker images even tinier than ever before: Liberica JDK 11.0.12 Alpine Linux musl — 75.82 Mb Liberica JRE 11.0.12 Alpine Linux musl — 44.19 Mb Liberica JDK 11.0.12 java.base — 21.7 Mb! Fill out the form below, consult with BellSoft expert engineers, and learn how you can apply the smallest Java containers in your project. Book a free consultation Upstream changes: highlights of 11.0.12 1. AArch64 backports Recent engineering work to improve JDK performance on Arm has been backported to v 11.0.12. This includes 28 backports by BellSoft, which totals at 10% of all backports to the release. Such a high number couldn’t have been achieved without the contribution made by the Red Hat team first to add support2 for and then optimize3 LSE Atomics in C++ code. Overall, the backporting has led to a 6% performance increase in DaCapo. 2. JNF dependencies removed JavaNativeFoundation is a framework introduced by Apple to support Java applications that use native methods. It is not a part of macOS but a set of macros and C functions to ensure interoperability with the Cocoa framework: e.g., converting between Java strings and NSStings. Its macros clean up memory after exceptions, rethrow Objective-C exceptions as ones on Java, and deal with Autorelease pools. Previously, using this component wasn’t mandatory but neglecting it may have resulted in illusive errors. However, it is no longer maintained and needed in the JDK. In this release, JNF dependency is excluded, and the JavaNativeFoundation Framework is no longer bundled with Liberica JDK 11, both on x86_64 and AArch64. The removal will go completely seamless to a Mac user. Just install the new version — no changes are required. 3. G1 backports The current security release improves G1 performance for applications that produce large numbers of short-living humongous objects. Most of such objects are collected during the young gen collection phase. Therefore, the number of humongous regions that are presented after the last GC, newly allocated since the last GC, or presented after this GC are recorded on each young-only collection cycle.4 These values are used in the adaptive IHOP calculation.5 You can trust BellSoft to listen to the many voices of the OpenJDK community. We are focused on providing our users with handy features, consistently and always on time. The new builds are ready! Use this link or click the button below to head over to Liberica JDK Download Center. Download Liberica JDK References Spring Native on GitHub [JDK-8263876] AArch64: Support for LSE atomics C++ HotSpot code - Java Bug System [JDK-8263877] AArch64: Optimize LSE atomics in C++ code - Java Bug System [JDK-8246274] G1 old gen allocation tracking is not in a separate class - Java Bug System [JDK-8245511] G1 adaptive IHOP does not account for reclamation of humongous objects by young GC - Java Bug System - [Java 17 new features and tools: Pedal to the metal!](https://bell-sw.com/announcements/2021/08/11/pedal-to-the-metal-java-17-tools-and-features-to-expect/): When we think of new Java™, we like to imagine it being like a sleek modern car from the respected series — true to its roots, but fast, comfortable, and updated with all the gizmos needed for the ride. Sure, the newer car models are introduced constantly, but when you need both reliable and practical — you can’t go wrong with this one. So join us in exploring all the new roadways open with Java 17! Java means business What’s new in Java 17? Added features Removed features Upgraded features A few more words about Vector API New licensing policy Changing gear to 17-th The race is on! Java means business Java works best when you don’t want to choose between stability, security, and support. Many years of production with developers all over the world contributing to the upstream (including those from BellSoft) means that possible security issues are found and fixed fast, the development tools are continuously upgraded, the new features are added constantly, and the outdated components are removed. Java still remains the best multi-platform solution. So using Java is the most reliable choice for business, and usually the newer the version, the better. If you are unsure about updating, contact us, and BellSoft’s engineers will examine your architecture and share their detailed expert opinion on the issue. Book a consultation In any case, you can’t go wrong with Java. So hop on, as we are about to discover what was changed in the latest Java version. What’s new in Java 17? The modern car should be devoid of the older design elements with no unnecessary parts to limit its speed but remain comfortable and powerful. And it seems the JDK community follows these guidelines too. This is a Long Term Support release, which means it will remain an industry standard for years, and receive updates for a long time. Java 17 will receive updates up till 2026. As usual for the LTS release, some things were deprecated, some features were added, and the most valuable components received an upgrade. Let’s explore all the Java 17 news. Added features Something new to go even faster. Context-specific deserialization filters (JEP 415) to configure the filters via a JVM-wide filter factory. Developers can select a filter for each individual deserialization operation. The deserialization filters technology was introduced in Java 9 to enable application and library code to validate incoming data streams before deserializing them. The new context-specific approach will significantly enhance security by providing the ability to remove potentially malicious code from the deserialization process. The foreign function and memory API (JEP 412) is introduced to replace the Java Native Interface (JNI) with a superior, pure Java development model. This feature will provide better Java performance, the ways to operate on different kinds of foreign memory, and better security by disabling unsafe operations by default. In the future, it is expected to allow the usage of foreign functions written in languages other than C. JNI still functions though, so the apps depending on it will still run in Java 17. Always-strict floating point semantics (JEP 306) are restored the way they used to be in Java versions before 12. The decision to change the default floating-point semantics was made due to the restrictions of the x86 architecture of the 1990s. Since CPUs started supporting SSE2, this issue is long-solved, and making floating-point operations consistently strict will ease the development of numerically-sensitive libraries, including Java.lang.Math and java.lang.StrictMath. Pattern matching for switch (JEP 406) is implemented as a preview, allowing an expression to be tested against several patterns, each with a specific action. All existing switch expressions and statements will still compile. Two new kinds of patterns are introduced. A new rendering pipeline for macOS (JEP 382) will use the Apple Metal API instead of the older OpenGL. This feature is particularly important as there is a chance Apple will remove OpenGL API from future macOS versions. It will fit the existing Java 2D pipeline model and provide at least the same performance as the current rendering pipeline or better. Removed features The older tids and bits that only slowed you down had to go. Removal of the experimental AOT and JIT compiler (JEP 410). The ahead-of-time (AOT) and just-in-time (JIT) compiler was added to Java as an experiment some time ago but turned out to not be popular. As its exclusion from the JDK 16 went unnoticed, it has been decided to remove the compiler altogether, including the following modules: jdk.aot (the jaotc tool), internal.vm.compiler (the Graal compiler), and jdk.internal.vm.compiler.management (the Graal MBean). Note that while Graal is removed from Java 17, it is still an essential tool for many features, so it will keep being developed and included in builds for native images like Liberica Native Image Kit. Removal of the Remote Method Invocation (RMI) Activation mechanism (JEP 407), but keeping all the rest of RMI intact. The activation mechanism is dated and disused. Deprecating the Applet API for removal (JEP 398). Since all the browsers have either removed support for Java browser plug-ins or plan to do so, this now useless API also has to go. In practice, some compiled and used apps still depend on that API, which is why the API itself is still present in Java 17. If that is true in your case, you may need to stick to Java 8 (supported up to 2022) or 11 (supported up to 2023). Deprecation of the Security Manager (JEP 411) for removal in a future release. This component has had a good run, as it dates all the way to Java 1.0 and was primarily used to secure the client-side code. The developers are expected to adjust the modern security solutions to do this task now. The issue is that the apps still using the Security Manager will require a lot of code rewriting when the component will actually be removed in Java 18. Since the default value of java.security.manager system property is now “disallow”, most of the tests calling System.setSecurityManager() need to be launched with -Djava.security.manager=allow. Upgraded features Some helpful tools that needed a little polish to shine again. Vector API (JEP 414), introduced in the previous Java version as an incubated API, is enhanced for better performance based on the feedback from developers. It will support operations on characters, transcendental and trigonometric lanewise operations on x64 using Intel’s Short Vector Math Library (SVML), and better translation of byte vectors to and from boolean arrays. We will later discuss the implication of this feature in-depth. Strong encapsulation for all JDK internals (JEP 403), except for critical internal APIs. The idea is to forbid relaxing the strong encapsulation of internal elements via a single command-line option. This, in turn, should lead to better security and encourage developers to use standard APIs instead of internal elements. In the end, it will make upgrading to the future Java versions much more effortless. The downside is that if the existing app or framework is dependent on the internal API, it may require a lot of code rewriting or even opting out of updating to Java 17. Note, that sun.misc.Unsafe and reflect classes and add-opens flag still work and may be used in the future. Sealed classes and interfaces (JEP 409) that restrict which other classes or interfaces may extend or implement them. Delivered before as a preview feature, they give developers more control over the code responsible for implementing the classes they made. Porting the JDK to MacOS/AArch64 (JEP 391). As we already discussed, Apple’s Mac computer’s transition to AArch64 architecture is in full progress, so the port is necessary for macOS. Note that Linux and Windows AArch64 ports are already available, and a lot of existing code is expected to be reused by employing conditional compilation. Enhanced pseudo-random number generators (JEP 356) is implemented with the new RandomGenerator interface and a uniform API for all RNG algorithms, including three already implemented. A few more words about Vector API To see the real impact of Vector API implementation in Java™, let’s do some research. The following test code will calculate sin(double) over an array of input arguments. This function is already implemented as a HotSpot intrinsic for x86_64. The question is, can it work even faster when using SVML? We’ll use the following jmh-based code to compare “sin” calculation with and without using VectorAPI: scalarSin vs vectorSin /* * Copyright (c) 2021, BELLSOFT. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). */ import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import jdk.incubator.vector.VectorSpecies; import jdk.incubator.vector.VectorOperators; import jdk.incubator.vector.DoubleVector; @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(3) @State(Scope.Thread) public class VectAPI { static final VectorSpecies SPECIES = DoubleVector.SPECIES_PREFERRED; @Param({"8", "16", "64", "256", "1024", "4096", "16384"}) int size; double[] in; double[] out; @Setup(Level.Trial) public void initArrays() { in = new double[size]; for (int i = 0; i < size; i++) { in[i] = i; } out = new double[size]; } @Benchmark @Threads(1) public double[] scalarSin() { for (int i = 0; i < size; i++) { out[i] = Math.sin(in[i]); } return out; } @Benchmark @Threads(1) public double[] vectorSin() { int i = 0; int upperBound = SPECIES.loopBound(size); for (; i < upperBound; i += SPECIES.length()) { DoubleVector va = DoubleVector.fromArray(SPECIES, in, i); DoubleVector vb = va.lanewise(VectorOperators.SIN); vb.intoArray(out, i); } for (; i < size; i++) { out[i] = Math.sin(in[i]); } return out; } } Running this benchmark on Xeon(R) Platinum 8268 CPU @ 2.90GHz gives following results Benchmark (size) Mode Cnt Score Error Units VectAPI.scalarSin 8 avgt 15 82.495 ± 1.274 ns/op VectAPI.scalarSin 16 avgt 15 170.337 ± 1.346 ns/op VectAPI.scalarSin 64 avgt 15 679.862 ± 7.345 ns/op VectAPI.scalarSin 256 avgt 15 2700.459 ± 34.977 ns/op VectAPI.scalarSin 1024 avgt 15 11511.768 ± 194.371 ns/op VectAPI.scalarSin 4096 avgt 15 45569.034 ± 477.101 ns/op VectAPI.scalarSin 16384 avgt 15 188526.742 ± 1008.427 ns/op VectAPI.vectorSin 8 avgt 15 12.815 ± 0.284 ns/op VectAPI.vectorSin 16 avgt 15 22.056 ± 0.788 ns/op VectAPI.vectorSin 64 avgt 15 76.410 ± 1.329 ns/op VectAPI.vectorSin 256 avgt 15 297.379 ± 3.158 ns/op VectAPI.vectorSin 1024 avgt 15 1344.975 ± 51.351 ns/op VectAPI.vectorSin 4096 avgt 15 5817.973 ± 55.963 ns/op VectAPI.vectorSin 16384 avgt 15 24212.389 ± 915.261 ns/op In the end VectAPI.vectorSin is about 8 times better than VectAPI.scalarSin. After checking the compilation log (-XX:+PrintCompilation), we find that Double512Vector is used, which is exactly 8 doubles. It looks like linear scalability. Let’s take a look under the hood. The main action is “sin” calculations: “va.lanewise(VectorOperators.SIN)” code for vector case. It is compiled as a call to the svml library in the end. The libsvml.so is provided with x86_64 jdk17. It contains an implementation of several vectorized functions for different vector sizes. And C2 compiler inserts calls to these functions instead of default scalar implementation in case libsvml is available. Note that running benchmarks on older systems without AVX512 support (or emulating them by settings UseAVX=1 or UseAVX=2 to use 128-bit or 256-bit vectors) shows the worse benchmark scores proportionally to vector size reduction, resulting in an almost perfect linear scale. Disabling vectorized intrinsic by disabling UseVectorStubs to trigger scalar “sin” intrinsic shows slightly slower results than the scalar one due to additional overhead. Our test shows that the Vector API allows the convenient usage of SVML, and it works fast. It adapts to the target CPU capabilities ( in this case - using different vector sizes and the performance scales linearly with it). Scalar calculations using code around Vector API will work, but some overhead is expected. New licensing policy With the release of Java 17, Oracle announced changes in its licensing policy. LTS versions will now be released every two years, rather than three. Free updates for LTS releases will be provided for up to one year after the next LTS release and for non-LTS or “feature” releases, up to six months after the next release. We understand that this news will result in many questions. Does it mean that you do not need to pay for Oracle JDK any longer, provided that you upgrade in a timely manner? Does your company have the resources to switch to a new JDK version every two years? And the even more important question: should your company upgrade or not to Java 17? First, as you care about the security of your apps, upgrading is unavoidable; if not to Java 17, then to a newer version than your current one. You wouldn’t skip the servicing of your car, right? The same applies to your JDK. Newer versions contain security patches and updates that help protect you and your clients’ data. But which should you choose, Oracle JDK or OpenJDK? Free updates from Oracle sound tempting, but consider that after three years you will still be required to pay for Oracle’s support or upgrade every two years. Moreover, adapting to a newer JDK version takes 12 months on average, and before you know it, the next release will come knocking on your door. OpenJDK vendors also provide free support for much longer than Oracle. BellSoft, for example, will support JDK 17 until 2030! This way, you can enjoy free updates for the lifetime of your application and be sure of the stability and security of your runtime. Changing gear to 17-th We decided to ask our engineers to share their opinion on changes in Java 17. And that’s what they had to say: I believe that one of the most essential improvements in Java 17 is the deprecation of the Security Manager that goes hand in hand with strong encapsulation for JDK internal elements and foreign function and memory API. JVM is now launched in a strict mode where it is impossible to break internal API isolation, so inaccessible modules are well protected. This mechanism takes the JDK security system to a new level. Sergey Chernyshev Developers will especially appreciate pattern matching for switch and Vector API, as these features make life so much better! Your code becomes more clear, concise, and easier to write and modify. In my opinion with inclusion of the Vector API Java embarks on the path to a breakthrough that we may witness in the following versions. Dmitry Strizhikozin The introduction of context-specific deserialization filters in Java 17 is a big step forward in enhancing Java security. If you work with external data, such filters are going to be your key to fix a potential weak spot for hacker attacks. In addition, strong encapsulation of JDK internals aids in safer web development. Every developer will find something that best suits their needs among new features, be it Vector API for faster calculations or a new RNG with additional interfaces. And foreign function API and pattern matching for switch facilitate and speed up code writing. Dmitrij Pochepko The strong encapsulation for JDK internals will help developers avoid running into the situation when it’s hard to see how the older code in the app works. It lessens the risk of using potentially insecure undocumented APIs without understanding their inner workings. I wonder how the foreign function and memory API will turn out and if it will be as easy to implement the foreign code as promised. And the Mac Aarch port shows that Java devs are trying to stay relevant, which is a good sign for the future. Peter Zhelezniakov Want to hear more opinions on this issue and others? Contact us! The race is on! Java has always been a “monster car” — powerful, fast, and ready for any environment. As the years pass, it keeps gaining speed and focuses on providing the users with all the features you should expect from the modern programming language. So if you were looking for a sign to update or go Java — this is it! New builds of Liberica JDK, an OpenJDK-based Java distribution, are already available. Click the button below, download the version for your platform, and enjoy all the benefits of the latest Java 17 version! Get Liberica JDK - [Spring Native — the image of the future](https://bell-sw.com/announcements/2021/08/26/the-new-spring-is-here/): The Spring “family” is built around the Spring framework, one of the most popular Java frameworks in existence, with Spring Boot as a flagship. First released 18 years ago, it keeps growing and rejuvenating. While newer solutions appear, Spring stays on top. It is still a lightweight, secure, easily customizable, and flexible programming environment that has become a standard helper in modern Java development. But today, we would like to focus on the newest addition to the Spring “family” ー Spring Native, soon to be released. Now in beta, it has caught the attention of developers worldwide and is available for testing. Note that our own Liberica Native Image Kit is fully compatible with Spring Boot. You can read all about it below or turn to our engineers for free advice. Consult our team So what is Spring Native? Spring Native is a version of Spring Framework with built-in support for compiling applications into native images with GraalVM. It allows running Spring-based code on any system without a conventional Java Virtual Machine with almost instant startup, instant peak performance, and low memory consumption. Using AOT (ahead-of-time) compiler, Spring Native generates a small image containing the OS layer, dependencies necessary for running the code, and bits of OpenJDK and Spring, all packed into an executable file. In the end, it trades longer build times and, possibly, less runtime optimizations for faster startup, lower footprint and simpler container structure. That fits well for cloud deployment, management systems such as Kubernetes and even a serverless approach. Spring is getting cooler! So why does it matter? Glad you asked! There are actually several reasons: Spring is catching up with competitors, having solved one of the most critical issues of the framework. The GraalVM native image support was in Quarkus from the start, and Micronaut added it quite some time ago. Now Spring is also compatible with it and can compete in this matter. In the Cloud era, the size of microservices is more crucial than ever. It saves time AND money. The Spring applications tend to get bloated with time, and Spring Native solves this problem. Although still experimental, Spring Native is available on the official site of the developer to play with, and the images created with it already display faster startup times and lower memory footprints than Spring-generated jars. With the future Spring framework version release based on Java 17, you can expect many changes under the hood that will enhance the already excellent performance of Spring Native. It took so long to implement this technology because of Spring Boot and GraalVM’s very different behavior. The AOT compilation is not a great fit for Spring’s very dynamic, pluggable runtime behavior. This issue is solved with a bootstrap code generator (available with maven or gradle plugin). This is a huge milestone that allows us to enjoy the best of both worlds. Spring Native + Liberica Native Image Kit: Let’s try it! As we already mentioned, Spring Native is available for testing. While we can expect more improvements with the final release of Spring based on JDK 17, there is still a way to experiment with the performance of the feature. So let’s turn a jar into the native image! We will use the petclinic project as an example, our own Liberica JDK 11 as runtime, and Liberica Native Image Kit as a tool. Check out Liberica NIK The easiest way to start with Spring Native is to create an artifact using Spring Initializr or study examples at https://github.com/spring-projects-experimental/spring-native/tree/main/samples. In our case, we will use the petclinic-jdbc application. If you take a look at the main and parent pom.xml contents in a Maven project, there are few notable features: Dependencies include spring-native plugin (currently org.springframework.experimental:spring-native:0.10.3-SNAPSHOT). Build section includes spring-aot-maven-plugin and org.graalvm.buildtools:native-maven-plugin. There is a separate ‘native’ profile with its own native-maven-plugin configuration. org.springframework.boot:spring-boot-maven-plugin is configured to use a paketo buildpacks builder (paketobuildpacks/builder:tiny) which is in BP_NATIVE_IMAGE=true mode by default. There are multiple options on how to build and run the application. In any case, we will need a JDK to run Maven. I have Liberica JDK installed as a package on my Linux system, so the JAVA_HOME part in the command line is set accordingly. Classical JAR Let’s start with building a JAR file. JAVA_HOME=/usr/lib/jvm/bellsoft-java11-amd64/ ~/apache-maven-3.8.2/bin/mvn install It results in a 24 MB target/petclinic-jdbc-0.0.1-SNAPSHOT.jar. You can start it as usual: java -jar target/petclinic-jdbc-0.0.1-SNAPSHOT.jar Startup time on my laptop is about 2.4 seconds. Default Native Image container Let us build a container. Note that if you use Windows or macOS, you need to ensure that memory allocated to Docker is at least 8 GB. Run this command: JAVA_HOME=/usr/lib/jvm/bellsoft-java11-amd64/ ~/apache-maven-3.8.2/bin/mvn spring-boot:build-image We got a container image that you can launch with docker run -it docker.io/library/petclinic-jdbc:0.0.1-SNAPSHOT As it is native image-based, the startup time is about only 0.1 seconds. A massive difference from 2.4 seconds! To make our work even more convenient, let’s get a fully functional application with a selected database by using docker-compose and docker-compose.yml configuration: docker-compose up This method also turns into classical layered container image assembly by changing BP_NATIVE_IMAGE to false (JVM and .jar inside). Native Image Paketo buildpack described in the previous section uses a specific version of GraalVM and native-image. For example, in the current version it is GraalVM 21.0.0.2 (Java Version 1.8.0_282-b07). But we can use that “native” profile mentioned earlier to build the native image any way we want. For example, let’s use the Liberica Native Image Kit based on GraalVM 21.2.0 and Liberica JDK 11.0.12. First, download Liberica Native Image Kit Core. The Core version is best suited for Java native images, as it contains Liberica VM and native image (derived from GraalVM) without additional languages at the start. Then choose your platform and packaging. In my case, it is Linux x86_64 and .tar.gz, Liberica NIK version is 21.2.0. Now pass the full path of the installation in JAVA_HOME and invoke the “native” profile for the build: JAVA_HOME=$(pwd)/bellsoft-liberica-vm-core-openjdk11-21.2.0 ~/apache-maven-3.8.2/bin/mvn -Pnative install The process takes about 10 minutes to finish (it uses 8 CPU cores and about 8 GB of RAM). As a result, we get the 118 MB target/petclinic-jdbc binary. After testing it, we see it starting in just 1/10th of a second ー the very best result we managed to produce today! In this case we used a more recent version of native image tooling, than the default one. But sometimes there is a need to stay on a specific release until all migration issues are resolved, so this method could be used in reverse. Liberica loves Spring As you see, even in this experimental stage, Spring and Liberica NIK work pretty well together. The important thing to mention is that you will always be able to use the previous LTS versions of Liberica NIK if you are not ready to upgrade to the new Java 17. In this article, we discussed all the deprecations and innovations in the newest OpenJDK. If you are wary of backward compatibility, you can still use Liberica JDK 11, as it will get updates up to 2026. For more information, see the support roadmap. Discover Liberica JDK And while at the moment, we still require a lot of customization to make Spring Boot friends with GraalVM, that is about to change. In September, Java 17 will be out, and a new version of Spring Boot is bound to be released a bit later, making creating native images a much smoother process. As for now, we suggest you dive into Spring Boot, test it out and check how your Spring applications run fast like never before! - [A support agreement of VMware and BellSoft for Spring Boot](https://bell-sw.com/announcements/2021/09/02/BellSoft-VMware/): BellSoft, the provider of progressive Java Runtime for agile enterprises, and one of the leading OpenJDK contributors, is excited to announce its new agreement with VMware. Now, using Spring Native and Liberica Native Image Kit (NIK), developers will be able to produce Spring Boot native applications seamlessly. Thanks to this agreement, VMware Tanzu® customers using Spring Native will utilize Native Image Kit and a Liberica Java Runtime, which will enable Spring Boot developers to reduce the startup time of their services and memory consumption. Reduce TCO of Spring apps! As a result of this arrangement, through Cloud Native Buildpacks VMware will provide their users with a totally integrated experience including both native support for Spring and Liberica Native Image Kit in the popular Java buildpack. BellSoft has previously been providing JDK support to VMware for its customers. Satisfied with the constantly reliable and timely support of OpenJDK, VMware invited BellSoft to extend the relationship by providing compiler and Java Runtime support for Spring Native. Being a part of the VMware ecosystem of products, Spring already uses Liberica JDK. Liberica NIK Core is based on OpenJDK and GraalVM with no additional dependencies, and fully aligns with GraalVM’s release cadence. It also supports all standard platforms (Linux, Windows, Mac), and optional platforms, including AArch64. The absence of additional dependencies and languages caters to better security and maintainability. Liberica NIK also fully supports Alpine Linux, as both build and deploy platforms, thus allowing the reduction of container size compared to conventional Linux distributions. “Cloud infrastructure is becoming increasingly crucial in the modern development landscape. As many enterprises are entering the cloud, the Java community is adapting to their needs, creating solutions such as Liberica NIK, and integrating them into popular frameworks like Spring Boot,” says Alex Belokrylov, CEO of BellSoft. Liberica Native Image Kit (NIK) is a universal compiler based on the GraalVM Community Edition code base. NIK is designed to enable the implementation of multilingual projects, such as microservices. It allows Java developers to use libraries and frameworks written in other languages. Liberica NIK Core is a particular version of Liberica NIK that is restricted to only Java support. It is perfect for pure Java development, particularly when paired with Spring Native. About BellSoft BellSoft — a leading OpenJDK contributor provides security and progressive Java Runtime, Liberica JDK, for cloud and present-day architectures and modern tools for Java applications. BellSoft engineers have been contributing to the OpenJDK project since its inception.The company drives a community-powered approach to deliver reliable and compact containers with Liberica JDK targets microservice solutions on. Using popular as well as in-house performance optimization techniques and tools allows BellSoft to work on performance tuning for industry leaders. You can find more BellSoft news on the companies’ blog. About VMware VMware software powers the world’s complex digital infrastructure. The company’s cloud, networking and security, and digital workspace offerings provide a dynamic and efficient digital foundation to customers globally, aided by an extensive ecosystem of partners. Headquartered in Palo Alto, California, VMware is committed to being a force for good, from its breakthrough innovations to its global impact. For more information, please visit www.vmware.com/company - [Dive into the DevOps way of thinking at JRush](https://bell-sw.com/events/2021/09/13/Events-JRush/): The emergence of the DevOps concept in 2009 started a revolution in the IT world. Today we cannot imagine building perfect infrastructure without integrating DevOps principles and processes. As Java is one of the most popular programming languages used by many large enterprises, knowledge of DevOps practices is critical for Java developers. BellSoft invites seniors and top specialists to discover the latest trends in DevOps. Join us on Sept. 23, 11 am PT, online. Find out how to boost the performance of your apps and speed up infrastructure transformation. Register for JRush We continue our web conference series, JRush, where Java engineers get concise yet extensive information on a particular topic. Our first topic was the native image technology and its role in optimizing projects and minimizing resources. If you are interested in this subject, feel free to watch a recording of the event: .embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; margin: 15px 0; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } Our second web conference is dedicated to DevOps culture and associated technologies. In just 2 hours, you will get the most relevant information from renowned DevOps experts: Donovan Brown, Partner Program Manager in the Azure CTO Incubations team at Microsoft, will show you how to avoid security pitfalls and flawlessly move your whole workload into the cloud. Heidi Waterhouse, transformation advocate with LaunchDarkly, will share with you different ways of stabilizing your containers and developing discrete features. Dmitry Chuyko, BellSoft Senior Performance Engineer, will help you choose the optimal runtime for your company to minimize total costs. He will also tell you about a new, unique tool to control your PC fleet with a single dashboard. Register now — Rush with us! Register for JRush See you there! - [Liberica JDK 17 LTS release: solid ground for an update](https://bell-sw.com/announcements/2021/09/17/liberica-jdk-17-lts-release-solid-ground-for-an-update/): LTS means “Long-Term Support” release, but we sometimes call it “Long Time no See.” And while the LTS releases of Liberica JDK are introduced at a cadence fully aligned with OpenJDK releases, it’s a big event every time, which we await eagerly, so much so, that we have already told you about all the added, updated, and deprecated features in the new Java™. Every time there is a new LTS release, the question arises, “To upgrade or not to upgrade?” And while there are no radical differences between non-LTS and LTS releases, users tend to stick to the later ones, with 69% of them still using Java 8, 36% relying on Java 11, and only 16% enjoying the features of Java 12 and up (with some of the developers asked using more than one version).1 The new LTS release could be solid ground for an update, for many reasons, like the new features and security. In this article, we would like to focus on the essential changes, and try to give you some guidelines for updating in case you do make that decision. By the way, Liberica JDK 17 supports the Coordinated Restore at Checkpoint for reducing the startup and warmup times of Java applications from minutes to milliseconds. Download Java with CRaC for free and follow our guide on using CRaC with Java apps! Table of Contents Why choose Liberica as the LTS JDK? Liberica JDK 17 is more secure Liberica JDK 17 is more convenient Liberica JDK 17 is modern Liberica JDK has sweet spots To upgrade or not to upgrade How upgrading works Liberica JDK 17 is here Why choose Liberica as the LTS JDK? What makes Liberica JDK different from other OpenJDK builds, and from our competitors? Many things: Liberica JDK is released by the same people who develop OpenJDK. BellSoft is one of the top OpenJDK contributors, with multiple JEPs proposed and implemented. In OpenJDK 17 we fixed 7 issues out of 2743. In addition to the numbered versions, we release Critical Patch-only Updates (CPU) that keep your runtime secure. Liberica JDK has additional features not present in OpenJDK. We support many additional platforms, including AArch64, lightweight Alpine Linux, and many others. Our enterprise support is provided by the same engineers who have developed Liberica JDK, and it is available 24/7/365. No middle-men, no long wait times. We provide support to VMware Tanzu users and developers. Spring Framework 6 and Spring Boot 3-based applications will require a minimum of JDK 17 at runtime. It will be the JDK version used in Spring Framework You can get Liberica JDK 17 and enjoy all the benefits right now! Get Liberica JDK We have already covered all the JEPs in depth, so let’s talk about the most important new features. Liberica JDK 17 is more secure There are many vital additions that give JDK security a significant boost. With the strong encapsulation for JDK internal elements (JEP 403), developers avoid running into a situation when they rely on undefined behavior not specified in the public API. It lessens the risk of using potentially insecure undocumented APIs without understanding their inner workings. It also aids in safer development. In our opinion, this is one of the major updates that will influence Java™ development for many years. The introduction of context-specific deserialization filters (JEP 415) is the key to fixing a potential weak spot for hacker attacks. The deprecation of the outdated Security Manager (JEP 411) will encourage developers to move to modern security solutions. Liberica JDK 17 is more convenient It is now easier to write in Java™, so you may get a speed development productivity boost! The pattern matching for switch (JEP 406) and enhanced Vector API (JEP 414) will help make code clearer, more concise, and easier to write and modify. Using the Vector API can lead to faster calculations, and pattern matching for switch will facilitate and speed up code writing. With the foreign function and memory API (JEP 412) the usage of external native (non-Java) libraries is more convenient. JNI will no longer be necessary for that. Sealed classes and interfaces (JEP 409) will give developers more control over the code responsible for implementing the classes they write. With always-strict floating point semantics (JEP 306), the original floating-point semantics of the language and VM (changed in Java SE 1.2) are restored, providing more regularity in a tricky aspect of the platform without causing a hit to performance. Liberica JDK 17 is modern The new JDK version adds the features that are expected in contemporary programming languages. The MacOS/AArch64 port (JEP 391) and a new rendering pipeline for macOS (JEP 382) are the answers to Apple’s decision of transitioning to different architecture, and using the Apple Metal API instead of OpenGL. It means that Java 17 is ready for the newest Macs. The enhanced pseudo-random number generators (JEP 356) is the addition that enhances the RNG API. A new interface, RandomGenerator, supplies a uniform API that allows you to utilize any RNG algorithm, including three that are already implemented. Deprecating the Applet API for removal (JEP 398) is another step to a modern development environment without using the outdated functions. Liberica JDK has sweet spots Liberica JDK is the instance of OpenJDK with a wide selection of supported platforms, including specific ones, allowing the build of the smallest containers. We include the LibericaFX (JavaFX instance) and offer the edition embedded. We provide many tools, including the Liberica Native Image Kit for native images, and there are more tools in development. Liberica has been used to power up Spring Framework and Spring Native since 2021. We provide free security patches 4 times a year, in addition to numbered releases. Our commercial support helps you with minimal response time, and without a middle-man. To upgrade or not to upgrade We are now back to the question of upgrading (or not). Is it worth it? In our opinion 一 yes, it is, and here’s why: In case you have been using the newer JDK versions (11 and up), it is hard to find a reason to miss the upgrade. You have likely rewritten your older apps already, and moving from JDK 15 to JDK 17, for example, will require little to no rework. If you are still on JDK 8, you are using the dated version that was released in 2014. Although in the case of Liberica JDK you will still receive the free security updates untill 2031 (or paid updates in case of some other vendors), you are still missing out on seven years of new features. This new LTS build is a great opportunity to move up, after now it will only get harder to rewrite your app’s code for the newer versions of JDK. When there is no choice but to upgrade, you will have much more work to do. If you use JDK 7 or lower, the time to upgrade is long past due. While Liberica JDK provides support to versions 6 and 7 until 2026, there are still many security issues that make running the apps safe only in a closed environment. For example, JDK 7 uses LTS 1.0 by default, making it very vulnerable to a lot of network attacks. We strongly recommend that you invest in upgrading your apps and your runtimes. Our engineers will advise you about upgrading the runtime and dealing with any issues that arise. If you run your apps in a closed environment, plan never to make any changes, and low peak performance, critical performance (latency), effective resource utilization in container environments and slow startup time and speed are not an issue, you may think that you are off the hook forever. However, there are still ways you may be harmed with maliciously crafted data for these apps. Upgrading is important for that reason alone. Contact us to upgrade While upgrading is a process, in our opinion it is the right decision. While you are still getting ready to move to JDK 17, we recommend using the runtime with the support you can rely on. Let’s discuss all of the issues with updating and the solutions to them. How upgrading works Java™ language has good backward compatibility. And if you use Docker or Kubernetes, then “upgrading” can be as simple as specifying the newer version in Docker image. Here is an example tutorial to give you an idea of how to select a suitable runtime in Docker. The main issue is whether your application has dependencies on APIs or functions that have been removed from modern versions of JDK or strongly encapsulated. Remember, these changes were made for a reason! The deprecated components are replaced when new, faster, and safer solutions are available to perform the same tasks. Note that the old functionality is never removed at once. The first step is always marking for deprecation. So if you update frequently, you’ll have plenty of time to get ready for such changes. The first step is making sure your apps will run. Resources like the Java version Almanach2 can help you with finding the removed functionality in your apps. The next step is updating your tools. For example, if you produce native images, check out the new Spring Native3, powered-up by Liberica Native Image Kit. Make sure your Gradle and Maven plugins are up-to-date. If your apps utilize JavaFX, get the full Liberica JDK package containing LibericaFX, as OpenJDK has not included JavaFX since Version 11. The same goes for Mission Control, which was removed from OpenJDK and is available from BellSoft. Another thing to be aware of is JEP 403, which encapsulates JDK internal APIs. You should upgrade all the dependencies involved and remove the outdated code, if possible. As a temporary measure, you can enable your application to access internals like --add-opens=jdk.sctp/com.sun.nio.sctp=ALL-UNNAMED Or --add-opens=jdk.httpserver/com.sun.net.httpserver=ALL-UNNAMED In the future, when the JEP 408 is implemented in the OpenJDK, there will be a better public alternative for the latter case. If you feel overwhelmed, remember that it will become more challenging with every new release. In many cases updating is a matter of days or even hours. If you need help, our experts are here to make the process as easy and flawless as possible. Liberica JDK 17 is here Now that we have discussed all the issues around updating, we are sure that the advantages for your business (for example, TCO reduction) outweigh the negatives by a lot. Our advice stands 一 get the new Liberica JDK, and enjoy all of the benefits for your developers, security team, and in your productivity! Get Liberica JDK https://www.jrebel.com/blog/2021-java-technology-report https://javaalmanac.io/ https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/ - [Quarkus and Liberica Native Image Kit: it takes two to make a difference](https://bell-sw.com/announcements/2021/09/22/quarkus-and-liberica-native-image-kit-it-takes-two-to-make-a-difference/): Several major Java frameworks, such as Spring Boot, support GraalVM Native Image out-of-the box. We discussed how to tranform Spring Boot applications into native images here. Now, let’s take a look at what Quarkus is capable of in the native image environment. Quarkus + Liberica Native Image Kit: turn it up! Quarkus is tailored to native images approach, which is perfect for deployment in the cloud and serverless development. You could be pleasantly surprised to find out that Quarkus efficiency can be boosted even more. How? Use it with a tool, created especially for accelerating your applications ー Liberica Native Image Kit. Liberica Native Image Kit (NIK) is a GraalVM-based technology that converts a JVM-based application into a fully AOT compiled native executable under the closed-world assumption with an almost instant startup time. It supports a wide variety of platforms, including lightweight musl-based Alpine Linux. Full-fledged support for Alpine Linux musl makes Liberica NIK a perfect choice if you strive to minimize resource consumption, achieve record startup times (up to 1/10 of a second), and save on memory. Moreover, NIK allows you to create seamless polyglot projects in different programming languages. In addition, NIK runs with most JDK versions, which gives you full control over your build. And the fact that it is based on LTS Liberica JDK means that you can enjoy at least 8 years of access to bug fixes, security updates, and other fixes as needed. Obviously, you can’t go wrong with the combination of these two powerful tools, Quarkus framework and Liberica NIK. Without further ado, let’s see how to run them together. How to use Quarkus and NIK together? Before building a native executable for Quarkus, we need to get all necessary tools and software ready. First, set up a working C compiler toolchain. On Linux, GCC, and the glibc and zlib headers are necessary. * # dnf (rpm-based) sudo dnf install gcc glibc-devel zlib-devel libstdc++-static # Debian-based distributions: sudo apt-get install build-essential libz-dev zlib1g-dev On Windows, you will have to install the Visual Studio 2017 Visual C++ Build Tools. In the case of MacOS, the dependencies are provided by XCode. xcode-select --install To demonstrate how to integrate Liberica NIK into Quarkus, we are going to use the basic Hello World Quarkus application. Go ahead and follow the instructions on the site to create a simple Quarkus app. Now that you have your Hello World application ready, let’s configure Native Image Kit. Start with downloading the appropriate version of Liberica NIK (the package already contains Liberica VM, native image and language installables). After the download is finished, check the file by verifying the checksum in the command line (the checksum should match the one next to the link on the downloads page). Configure the runtime environment. For Linux and macOs, in case you have .tar.gz / zip archive, set JAVA_HOME environment variable to the NIK installation directory: export JAVA_HOME=$HOME/Development/bellsoft-liberica-vm-openjdk11-21.2.0/ If the installation is performed using the package (deb, pkg/dmg), the installation path for macOS will be the conventional one. On Windows, you will have to go through the Control Panel to set your environment variables. Install the native-image tool using gu install. Alternatively, you can download NIK Core version, which contains Liberica VM and native image without additional languages. In this case, use ni install: $JAVA_HOME/bin/gu install native-image As a result, the native executable for your application will contain the application code, necessary libraries, Java APIs, as well as a reduced version of a virtual machine. If you followed the instructions and constructed the Quarkus application, you will find the following profile in the pom.xml: native native To create a native executable, run ./mvnw package -Pnative. Note that it may take some time (several minutes usually) to package the native executable, so be patient. Here we are! You have built a native executable for your application! Find the resulting executable file as target/getting-started-1.0.0-SNAPSHOT-runner. To make sure that everything works correctly, launch the application. You should see the following: The application will listen on http://localhost:8080. Conclusion In this article, we discovered the benefits of using Quarkus framework to build cloud-native Java applications and learned how to spike its performance with Liberica Native Image Kit. The process of integrating these technologies is not that complicated, but in the end you get a triple bonus: Small image size to build tiny containers; Outstandingly faster startup time; Significantly reduced memory usage. Introduce your application to this superhero combo and you won’t be disappointed! - [Create a microservices constellation with Micronaut and Liberica Native Image Kit](https://bell-sw.com/announcements/2021/09/29/create-a-microservices-constellation-with-micronaut-and-liberica-native-image-kit/): The microservices are taking over the IT infrastructure; this is a well-established fact. Gigantic monolithic applications are vestiges of the past. With the dawn of new technologies, i.e. frameworks, companies can now create a whole constellation of microservices. The most popular framework is Spring Boot ー a renowned and reliable framework that makes it easy to create Spring-based applications. However, there are a number of alternatives available on the market. In this article we are going to focus on one of them: Micronaut. If you don’t feel confident enough at this point about microservices, contact us. Our engineers will be happy to consult with you on the matter, or help you with the migration from Oracle JDK to Liberica JDK. Consult our team Introducing Micronaut Micronaut is a JVM-based framework which was developed by Object Computing, Inc. The company stood behind the powerful Groovy-based framework, Grails. Micronaut can be seen as the successor to Grails. It supports three popular languages, Java, Kotlin, and Groovy (with Scala on the roadmap), and was created to simplify the development of microservices. Therefore, it includes all of the tools and features necessary for building a cloud-native microservice application: Compile time dependency injection and inversion of control Support for reactive programming Service discovery AOT compilation HTTP routing HTTP Client with client-side load-balancing Seamless API visibility Micronaut strives to overcome the issues of other frameworks by providing a fast startup time, a smaller memory footprint, minimal use of proxies and reflection, and fast and easy unit testing. In addition, Micronaut supports the creation of AWS Lambda functions, which is very useful for building serverless applications. If you have been using Spring up until now, you will find it easy to learn Micronaut. Now, let us try to build a microservice using this framework. Creating a microservice First of all, you need to install Micronaut. You can download a binary file from the official Micronaut site or use any package manager you like: Homebrew, Chocolatey, or SDKMAN!. For example, if you are using SDKMAN!, simply run the following command in your terminal: sdk install micronaut This will install all the binary files you need to create, test, and deploy Micronaut applications, together with Micronaut CLI. Next, create an application (Java will be used by default if you don’t specify the --lang argument): mn create-app --features=graalvm example.micronaut.micronautguide --build=maven Note: in case you are using Intellij IDEA, you have to enable annotation processing! Let us create a basic application with a list of hospitals. The completed example of this application can be found on the official Micronaut website. However, we recommend you to go through the step-by-step process of creating it. Create a Hospital class and annotate it with @Introspected: Create a Service and annotate it with @Singleton. Write a method that returns a random hospital: Next, create a Controller, which will return a Hospital. Micronaut will automatically convert it into JSON format in the response: Now your application is all set and ready. You can launch it as it is, but in the following section, we will show you how to do that using Liberica Native Image Kit. Integrating Liberica NIK Now that we have created our application, we need to generate a Micronaut native image with Liberica Native Image Kit (NIK). Liberica NIK is a GraalVM-based technology, which allows you to create seamless polyglot projects in different programming languages. NIK is a perfect choice if you wish to minimize resource consumption, achieve record startup times (up to 1/10 of a second), and save on memory. It is based on LTS Liberica JDK with 8 years of access to bug fixes, security updates, and other fixes as needed. Discover Liberica JDK Start with downloading the appropriate version of Liberica NIK (the package already contains Liberica VM, native image and language installables). Alternatively, you can download NIK Core version, which contains Liberica VM and native image without additional languages. After the download is finished, check the file by verifying the checksum in the command line (the checksum should match the one next to the link on the downloads page). You can also download Liberica NIK using SDKMAN!. Note that this package contains the full version of NIK, not the Core one. Run: sdk install java 21.2-nik Configure the runtime environment. For Linux and macOs, in case you have .tar.gz / zip archive, set JAVA_HOME environment variable to the NIK installation directory: export JAVA_HOME=$HOME/Development/bellsoft-liberica-vm-openjdk11-21.2.0/ If the installation is performed using the package (deb, pkg/dmg), the installation path for macOS will be the conventional one. On Windows, you will have to go through the Control Panel to set your environment variables. Install the native-image tool using gu install: $JAVA_HOME/bin/gu install native-image As a result, the native executable for your application will contain the application code, necessary libraries, Java APIs, and reduced version of a virtual machine. After you have installed NIK, you can generate a native image by running the following command: ./mvnw package -Dpackaging=native-image You can create a Docker image by running ./mvnw package -Dpackaging=docker-native After that, you can start the application either by running the native executable or using Docker. As you see, the application starts in 1.3 seconds, which is an impressive result. Conclusion Micronaut is the framework that makes building services easy. Remember that it also supports Kotlin and Groovy, so you can have several microservices written in different languages and united under the same roof of Consul Discovery Service. Thus, your constellation can grow into a microservice galaxy! As we have discovered, building native images with Liberica Native Image Kit is fast and convenient, and in the end you will enjoy the incredibly rapid startup of your app. If you want to learn more about the functionality of Liberica NIK or the benefits of small containers for your business, feel free to get in touch with our engineers. Together we will spike the performance of your application to the clouds and beyond! Book a free consultation - [JCMD everywhere — locally, containerized, and remotely](https://bell-sw.com/announcements/2021/10/14/jcmd-everywhere-locally-containerized-and-remotely/): “jcmd” is the command line executed with JVM diagnostic tools shipped with OpenJDK. Before the introduction of jcmd, there were multiple tools to run live and post mortem diagnostics for JVM. jcmd became a to-go utility for live diagnostics of JVM executed from the command line (e.g., dumping threads or inspecting JVM configuration). The post mortem diagnostics are performed with other tools. Contents Why do the command line tools matter? Common tasks for JCMD Example of jcmd usage Challenges jcmd and containerized JVM: It just works! Why does jcmd not work? What if you still need to use jcmd? JVM attach protocol under Linux Discovery Commands jattach — a light alternative for jcmd Sending jcmd commands via JMX Remaining caveat — accessing dump files Complete list of jcmd commands Conclusion My articles Why do the command line tools matter? There exists a wide range of tools in the JVM ecosystem, including graphical and web, so why use the command line tools? There are a few reasons: Command line tools can be executed with a shell / SSH terminal, and an SSH terminal could be the only secure way to work effectively with the JVM; Command line tools from JDK do not need upfront configuration and rely on OS access control for security; Command line tools are convenient for scripts and other means of automation you may introduce. Common tasks for JCMD jcmd displays a long list of commands (which is included at the end of the article), and a lot of them work on the low level of programming. Here is an overview of operations available via jcmd: Capturing thread and heap dumps — this is a pretty common task for Java engineers. Such operations were available long before jcmd was introduced and were utilized by means of the dedicated tools jstack and jmap. Control of flight recorder — jcmd has multiple commands to start/stop/dump JFR (JDK Flight Recorder) sessions. JFR is a powerful profiling and diagnostic subsystem in OpenJDK that we already discussed. Inspection of various aspects of JVM configuration — with jcmd you can check the HotSpot JVM runtime setting, system properties, and command line parameters. Control of JMX socket at runtime — using jcmd, you can open a socket accepting JMX connection without JVM restart. This is useful if you want to use GUI tools such as Mission Control while avoiding configuring the JMX upfront. These are the most common operations with jcmd utilized in the daily life of a Java engineer. Example of jcmd usage Next, we will discover the typical jcmd workflow with examples. If you are familiar with jcmd, you can skip the following section. Usually, jcmd is added to PATH when installing OpenJDK. If for some reason, it does not exist in your path, you can find binary under bin at your OpenJDK installation directory. The first thing jcmd requires is the PID of the JVM process you want to work with. There are multiple ways to get the PID of a JVM, and one of them is to list locally running JVM processes with jcmd itself. Executing jcmd without parameters will list locally running JVMs and their respective PIDs. > jcmd 23876 sun.tools.jcmd.JCmd 32311 HelloJDK As you see, jcmd includes itself in the list, because it is implemented in Java. Once you’ve got a PID, you can list commands exposed by the JVM. > jcmd 32311 help VM.unlock_commercial_features JFR.configure JFR.stop JFR.start JFR.dump JFR.check VM.native_memory ManagementAgent.stop ManagementAgent.start_local ManagementAgent.start VM.classloader_stats GC.rotate_log Thread.print GC.class_stats GC.class_histogram GC.heap_dump GC.finalizer_info GC.heap_info GC.run_finalization GC.run VM.uptime VM.dynlibs VM.flags VM.system_properties VM.command_line VM.version help The list of commands may vary between versions of JDK. Taking a thread dump is a common task beneficial for quick diagnosis. You can execute the “Thread.print” command for that. > jcmd 32311 Thread.print 32311: 2021-10-06 20:25:43 Full thread dump OpenJDK 64-Bit Server VM (25.302-b08 mixed mode): "Attach Listener" #9 daemon prio=9 os_prio=0 tid=0x00007f34c4001000 nid=0x5dbc waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007f35040d1000 nid=0x7e44 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread2" #7 daemon prio=9 os_prio=0 tid=0x00007f35040c3800 nid=0x7e43 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f35040c2000 nid=0x7e42 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f35040bf000 nid=0x7e41 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f35040bc000 nid=0x7e40 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f3504088000 nid=0x7e3f in Object.wait() [0x00007f34efaf9000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076e208ee0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144) - locked <0x000000076e208ee0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216) ... Another typical action is dumping a heap of JVM. > jcmd 32311 GC.heap_dump `pwd`/dump.hprof 32311: Heap dump file created The command above will create a “dump.hprof” file in the current directory. You can open this file with VisualVm or Eclipse Memory Analyzer Tool for detailed analysis. Notice that I provided the absolute path (via pwd command). The relative path is interpreted from the working directory of JVM, which is not the best place for heap dump creation. If you struggle to recall arguments for a specific command, you can use built-in help. > jcmd 32311 help GC.heap_dump 32311: GC.heap_dump Generate a HPROF format dump of the Java heap. Impact: High: Depends on Java heap size and content. Request a full GC unless the '-all' option is specified. Permission: java.lang.management.ManagementPermission(monitor) Syntax : GC.heap_dump [options] Arguments: filename : Name of the dump file (STRING, no default value) Options: (options must be specified using the or = syntax) -all : [optional] Dump all objects, including unreachable objects (BOOLEAN, false) -gz : [optional] If specified, the heap dump is written in gzipped format using the given compression level. 1 (recommended) is the fastest, 9 the strongest compression. (INT, 1) An excellent addition in OpenJDK 11 and above is the -gz option which allows the making of a compressed heap dump. Heap dumps compression is usually effective, saving both space and disk IO. This is a typical usage of jcmd, and I’m utilizing it reasonably often in my daily job. Challenges When developing and running Java applications on your desktop, jcmd works excellent. The main issue is running jcmd as the same user as the JVM you are connecting to. jcmd and other JDK tools work seamlessly on all supported platforms. But in modern times, you often have to run software in the containers or on some kind of remote host. jcmd is essential for ad hoc diagnostics of Java applications, and the ability to use it in a variety of circumstances is vital. Running a JVM in a container is pretty standard these days, so let’s see how jcmd deals with containers. jcmd and containerized JVM: It just works! If you will get a fresh Liberica JDK 17 image and use it with docker, jcmd would very likely work as expected (you will need to run jcmd as root). Although it was not the case not so long ago, JDK made tremendous progress with the native support of Linux containers over the last few years. Get Liberica JDK But let’s say something goes wrong, and you need to take a thread dump. What next? Why does jcmd not work? To solve the issue, let’s check a few things first. Are you using Linux? Linux containers run only on Linux OS. If you utilize macOS or Windows, your container is actually running in Linux VM under hypervisor on your host OS. jcmd uses IPC and cannot access processes running under different OS. Are you running jcmd as root? Jcmd should be able to connect to target JVM using IPC to function. That means it should either run under the same uid or as root. What version of Java are you running? When you execute jcmd from the console, you are using the system default JDK installation. Verify its version with java --version command. You need to have JDK 11 or higher on your host to work with containers. Your container could use an older version of JDK, although jcmd will still be able to see it. What version of Linux kernel do you have? You need at least a 4.1 version of the Linux kernel because it is the release in which some container-related data in procfs was introduced, and it is essential for the functionality of jcmd. These are the most common reasons jcmd could not be working. What if you still need to use jcmd? There are several options available in this situation. Run jcmd inside the container — this is the most straightforward way. If you can open the terminal in a container and run jcmd there, it would just work. The caveat here is that jcmd could be missing in the container environment. Run jcmd on the host Linux VM. If you are on Windows or macOS, try opening the terminal in the Linux VM hosting your container and run jcmd there. Execute command remotely with JMX. It is possible to execute a jcmd command remotely with JMX. While it requires upfront configuration, it could be a good option once you have finished with local development and start moving things to the target platform. JVM attach protocol under Linux Before moving forward, I would like to give some background on the inner workings of jcmd. There are two protocols implemented in jcmd — discovery and commands. Discovery Once started, JVM automatically creates a file in /tmp/hsperfdata_/ (id does the same for non-Linux OSes, though the path may be different). Older versions of jcmd were just scanning /tmp/hsperfdata_ to get the list of PIDs. That doesn’t work well with containers as /tmp in the container may not be the same as /tmp of the host. Starting with JDK 11, jcmd scans the list of processes and checks the presence of the hsperfdata file for each process (using respective user and pid). Thanks to the new approach, jcmd running under root can now see JVM from any user regardless of container boundaries. Commands jcmd is using a Unix domain socket to establish a connection to the target JVM. JVM should open a socket for jcmd to connect. jcmd may trigger creating a socket by sending a QUIT signal to target JVM (don’t worry, your JVM will not shut down). Socket is created under the current directory of target JVM (or under /tmp) and named .java_pid. That socket uses a simple text protocol, jcmd sends a command and receives some output and status code back. As often occurs with Linux containers, things are not what they seem. The same JVM process may have different PIDs (e.g., PID 1 in a container and some 12345 on host OS). This was fixed for jcmd in JDK 11, and now the container boundary is not a barrier for this tool. jattach — a light alternative for jcmd jattach is a compact tool written in C by Andrei Pangin that implements the protocol described above. You can find it at https://github.com/apangin/jattach. jattach is very handy if jcmd is not available for some reason (e.g., you have to use stripped-down JRE instead of JDK). Unlike jcmd, which you cannot just copy as a binary, jattach is a standalone binary of just a few kilobytes. It is easy to download and run when you need it. With jattach "jcmd …" you can enjoy the full functionality of jcmd from JDK. Note that you need to input the whole command starting with jcmd in quotes. Sending jcmd commands via JMX I have mentioned the lack of need for upfront configuration as one of the advantages of jcmd. Sometimes though, JMX access is available while terminal access is not. In this case, you cannot use jcmd, but precisely the same list of commands is available via JVM. I suggest using Mission Control by BellSoft. Get Mission Control Execute Mission Control, connect to the remote process with JMX Console and switch to the “Diagnostics Commands” tab. As you see, the very same list of commands from `jcmd` is present there. Commands are exposed as MBean operations, so they are available for other tools too. The “typical” JConsole. Operations are available under com.sun.management / DiagnosticCommand MBean. Unfortunately, JConsole cannot deal with argument types, so only the operations without parameters can be invoked. SJK is another open source tool for JVM diagnostics. It is capable of invoking any MBean operations through a JMX connection. If you need to invoke the jcmd command from the terminal on a remote JVM, you can use SJK. > java -jar sjk.jar jcmd -s host:port help The following commands are available: VM.unlock_commercial_features JFR.configure JFR.stop JFR.start JFR.dump JFR.check VM.native_memory VM.classloader_stats GC.rotate_log Thread.print GC.class_stats GC.class_histogram GC.finalizer_info GC.heap_info GC.run_finalization GC.run VM.uptime VM.dynlibs VM.flags VM.system_properties VM.command_line VM.version help For more information about a specific command use 'help '. Remaining caveat — accessing dump files Commands that produce files do that relative to the JVM working directory. If a JVM is running in a container, then the container’s FS will be used. If you need to take a heap dump of a JVM running in a container, it is better to use a path mounted from the host as a filename for the dump file. If no good mount is available, you can fall back to /proc/PID/root/ mount point to access the container’s file system from the host. Note that it will not help if the container is running in a VM or remote host. Complete list of jcmd commands While the list of commands supported by jcmd is long, many are more useful for JVM engineers than application developers. In any case, I would like to point out all of them. See the full list with descriptions Compiler.CodeHeap_Analytics (available since JDK 11) Dumps pretty detailed information about code produced by the JIT compiler, including addresses of compiled code blocks. Compiler.codecache (available since JDK 11) Prints code cache layout and bounds. Compiler.codelist (available since JDK 17) Prints all compiled methods in the code cache that are alive. Compiler.directives_[add, clear, print, remove] (available since JDK 11) Allows you to mess with the JIT compiler option on flight. Compiler.perfmap (available since JDK 17) Native Linux profilers expect debug symbol information for binary to be available in a specific format under /tmp/perf-.map. Without symbol file addresses from the stack, a trace cannot be translated to meaningful method names. Previously I have been using https://github.com/jvm-profiling-tools/perf-map-agent simultaneously with Linux perf for advanced profiling. With JDK 17 release, that functionality is built into JVM and exposed via jcmd. Compiler.queue (available since JDK 11) Prints methods queued for compilation. GC.class_histogram This command walks heap content (beware of Stop-the-World pause here) and dumps statistics aggregated by classes. Pretty valuable for spotting obvious memory hogs without firing up a full-featured heap analyzer tool. GC.class_stats (discontinued after JDK 11) Similar to the command above, but shows much more statistics. GC.finalizer_info Prints info about heap objects pending for finalization. GC.heap_dump Dumps content of JVM heap into a file for further analysis. GC.heap_info Prints details of heap memory spaces. GC.run Same as java.lang.System.gc() GC.run_finalization Same as java.lang.System.runFinalization() GC.rotate_log (discontinued after JDK 8) Triggers GC log rotation. Requires specific GC log configuration. JFR.* jcmd allows you to control JDK Flight Recorder sessions. You can start and stop recording sessions and dump results to file from the terminal with jcmd. JDK has another tool — jfr, which can help analyze the content of JFR recording without ever leaving the terminal. JVMTI.* (available since JDK 11) This command could be used to enable JVMTI profiling agent without JVM restart. ManagementAgent.* A handy set of commands, which enables the JMX port without restarting JVM. Typically, JMX port should be configured via JVM start up command. With jcmd, you can start the JMX port when you need it and start using JMX-based tools such as Mission Control, even if the JVM is not configured upfront. Thread.print Produces a JVM thread dump. VM.cds (available since JDK 17) CDS (Class Data Sharing) is a pretty old feature that has received many updates in JDK 12. Usually, shared class data archives are generated at JVM exit, if all required XX options were specified at the start. jcmd offers an alternative way to create such archives. VM.classloader_stats Prints statistics about all ClassLoaders. VM.classloaders (available since JDK 11) Prints a hierarchy of classloaders. Optionally can include a list of classes. This command could be very helpful for troubleshooting the “A cannot be cast to A” problem. VM.command_line Prints all arguments of JVM start command. VM.dynlibs Prints list of loaded native dynamic libraries. VM.events (available since JDK 17) Prints latest VM events. Events are produced by various JVM substems and may be useful for troubleshooting JVM issues. VM.flags Prints effective values of HotSpot JVM XX options. VM.info (available since JDK 11) Prints details similar to hserror crash dump, though without crashing the JVM :) VM.log (available since JDK 11) This command allows changing the configuration for JVM logging. VM.metaspace (available since JDK 11) This command can be used to retrieve various information about metaspace configuration and content. VM.native_memory This command works together with JVM native memory tracking. Tracking should be enabled on JVM startup. VM.print_touched_methods (available since JDK 11) Prints all methods that have ever been touched during the lifetime of this JVM. Requires -XX:+LogTouchedMethods options. VM.set_flag (available since JDK 11) The command allows you to modify manageable XX options of Hotspot JVM. VM.stringtableand and VM.symboltable (available since JDK 11) Prints statistics for string and symbol tables. VM.system_properties Dumps properties from java.lang.System.getProperties(). VM.systemdictionary (available since JDK 11) Prints statistics for system dictionaries per class loader. VM.unlock_commercial_features (discontinued after JDK 8) Rudimentary option for compatibility with older JDK. Noop in OpenJDK. VM.uptime Prints uptime of JVM. VM.version Prints JVM version details. PerfCounter.print This is a “secret” option, not listed in help, and not available via JVM. This command prints the content of the perf counter accumulated by the JVM. Unlike other jcmd commands, it does not require connecting to the JVM. Perf counters are exposed by JVM via /tmp/hsperfdata_/ file. Still got questions? Book a consultation with Java runtime experts from BellSoft. Get free advice Conclusion jcmd is a simple tool with a command line interface. You can use it for troubleshooting tasks, such as taking thread and heap dumps, introspecting JVM configuration, controlling flight recorder, and some others. Starting with JDK 11, jcmd is built with excellent support for Linux containers. In the case of using the containers, there are few key points to keep in mind: If you are on Windows or macOS, containers are running on Linux VM, and you have to run jcmd on the same VM for it to work; You usually need to run jcmd as root; jcmd from the host can see JVM processes in the container, but if launched within the container, jcmd will not see processes outside its environment. If you do not have jcmd installed on the host or in the container, jattach is a reliable and lightweight drop-in replacement that can execute the same exact commands. You also can run the jcmd command remotely via the JMX interface. Mission Control has a dedicated tab for jcmd in JMX Console; SJK supports sending jcmd command remotely via JMX from the command line. jcmd is a convenient utility that will be a great help in maintaining your VM, if you learn how to run and use it properly. My articles Check out my other articles on JDK Flight Recorder and JVM in Linux containers. JDK Flight Recorder – a gem hidden in OpenJDK (part 1) Hunting down code hotspots with JDK Flight Recorder (part 2) Hunting down memory issues with JDK Flight Recorder (part 3) JVM in Linux containers, surviving the isolation JDK Flight Recorder, The Programmatic Way JDK Flight Recorder vs. Stop the World Pauses - [Liberica JDK 8u312, 11.0.13, and 17.0.1 builds are out](https://bell-sw.com/announcements/2021/10/20/liberica-8u312-11-0-13-builds-are-released/): Contents Description How to keep your runtime secure The summary of fixes List of security issues fixed Notable changes in Liberica JDK Upstream changes: highlights Transfer to Git/Scara (JDK 11) Updated preference of the default enabled cipher suites Corrected response of methods related to CPU load (JDK 8 & 11) Fixed Kerberos credential retrieval in cross-realm setup Adjusted cgroup initialization (JDK 8 & 11) Supported platforms Enjoy the most stable runtime! Description This is a Critical Patch Update (CPU) release of Liberica JDK, the Open JDK instance produced by BellSoft. CPU patches are released quarterly for LTS versions of Liberica JDK (8, 11, 17) with a goal to keep the runtime secure and stable. These patches contain a number of Common Vulnerabilities and Exposure (CVE) fixes and defect fixes. In addition to CPUs, PSU releases contain non-critical fixes. The release contains 669 fixes and backports overall. Five security issues were fixed with the participation of BellSoft (3 in JDK and 2 in FX). How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updated and patches are available at no cost. The summary of fixes 13 security issues (CVEs) fixed; 29 total security fixes; 84 backports and bugs fixed: in Liberica 8u311: 27 + 2 in FX, in Liberica 11.0.12.0.1: 26 + 2 in FX, in Liberica 17.0.0.0.1: 25 + 2 in FX. In addition, PSU releases include a total of 543 bugs and backports fixed: in Liberica 8u312: 29 security fixes (27 + 2 in FX) + 109, in Liberica 11.0.13: 28 security fixes (26 + 2 in FX) + 303. in Liberica 17.0.1: 27 security fixes (25 + 2 in FX) + 47. Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2021-3517 8.6 javafx web network low none none unchanged low low high CVE-2021-35567 6.8 security-libs java.security network low low required changed high none none CVE-2021-35550 5.9 security-libs javax.net.ssl network high none none unchanged high none none CVE-2021-3522 5.5 javafx media local low none required unchanged none none high CVE-2021-35586 5.3 client-libs javax.imageio network low none none unchanged none none low CVE-2021-35564 5.3 security-libs java.security network low none none unchanged none low none CVE-2021-35561 5.3 core-libs java.util network low none none unchanged none none low CVE-2021-35565 5.3 core-libs java.net network low none none unchanged none none low CVE-2021-35559 5.3 client-libs javax.swing network low none none unchanged none none low CVE-2021-35578 5.3 security-libs javax.net.ssl network low none none unchanged none none low CVE-2021-35556 5.3 client-libs javax.swing network low none none unchanged none none low CVE-2021-35603 3.7 security-libs javax.net.ssl network high none none unchanged low none none CVE-2021-35588 3.1 hotspot runtime network high none required unchanged none none low Notable changes in Liberica JDK Liberica JDK has the widest range of supported platforms. This CPU update includes support for Windows 11, so you can update your Windows version and continue developing seamless applications. In addition, Docker images now have updated base images for Debian and Alpine Linux: Debian 10 Alpine Linux 3.14 CVEs fixed in Liberica per version: 17.0.1: 11 (9 + 2 in FX); 11.0.13: 12 (10 + 2 in FX); 8u311: 13 (11 + 2 in FX); 7u321: 9 (9 + 0 in FX). Upstream changes: highlights 1. Transfer to Git/Scara (JDK 11) With this CPU release, the JDK 11 updates project was moved to git on Github, so all Scara tools are now available for that release. The move to Git/Github is a significant step forward in terms of JDK11u maintenance, as it greatly simplifies the workflows for developers working on OpenJDK 11 updates. 2. Updated preference of the default enabled cipher suites Cipher suites are sets of algorithms aimed at securing a network connection through SSL or TLS. During the SSL handshake, client and server exchange the preferred cipher suites and choose the strongest one supported by both sides. It was proposed to update the default enabled cipher suites preference in order to prevent SSL stripping and similar attacks. The compatibility impact should be minimal. Forward secrecy should be preferable first. Moreover, new Chacha20-Poly1305 AEAD cipher suites are now supported in JDK 11. This is an important enhancement, as Google Chrome now prefers these suites most. 3. Corrected response of methods related to CPU load (JDK 8 & 11) The issues are related to containers. The first issue resides in the fact that the method getSystemCpuLoad() sometimes returned -1 when several CPUs on Linux machines were offline and cpusets.effective_cpus was absent. The second issue was that the method OperatingSystemImpl.getCpuLoad()could return 1.0 in a container, even though the CPU load was below 100%. The problem was caused by the usage of elapsed time instead of the total CPU time. The latter one is specified by cpu.cfs_quota_us, so the CPUs should be divided by “quotaNanos”. 4. Fixed Kerberos credential retrieval in cross-realm setup The problem was related to Kerberos constrained delegation when the backend and the middleware were in different realms. When the client requested the middleware, it received a ticket for constrained delegation to the backend, requested it on behalf of the client, and then the backend returned a response to the middleware. Due to the usage of tickets from the referrals cache, the first request succeeded; however, the subsequent ones did not. The solution is to not retrieve credentials from the cache for proxy requests. 5. Adjusted cgroup initialization (JDK 8 & 11) This particular problem was related to failing support of croups in Kubernetes: pods failed to start due to the exception that was thrown when reading cgroups information. The purpose of cgroups (or control groups) in Linux kernel is to allocate resources among processes (task groups) on a system. To initialize cgroups, it is necessary to find the mount points from /proc/self/mountinfo and read cgroup subsystem paths from /proc/self/cgroup. The latter file is a line-based text file with 3 fields, split by a colon. The file is parsed with a bare split, which results in cgroupPath containing a colon. In this case, the extra portion of the path is ignored, so the path is left as null during initialization. As a result, a NullPointerException is thrown at the attempt to read a configuration file and call Paths.getPath(). Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Yandex Cloud Enjoy the most stable runtime! We, at BellSoft, are committed to making our products more secure, efficient, and stable allowing you to continue developing the most high-end applications. You can download the new builds right now! Click here or on the button below to head over to Liberica Download Center. Download Liberica JDK Useful links [11u] Proposal: Switch jdk11u development to Git/Skara [JDK-8163326] Update the default enabled cipher suites preference - Java Bug System [JDK-8140466] ChaCha20 and Poly1305 TLS Cipher Suites - Java Bug System [JDK-8270137] Kerberos Credential Retrieval from Cache in Cross-Realm Setup - Java Bug System [JDK-8247469] getSystemCpuLoad() issue - Java Bug System [JDK-8265836] OperatingSystemImpl.getCpuLoad() issue - Java Bug System [JDK-8272124] Failing cgroup v1 initialization - Java Bug System - [Liberica Native Image Kit 21.3 based on Liberica 17 and 11 is out!](https://bell-sw.com/announcements/2021/10/22/the-new-liberica-native-image-kit-21-3-based-on-liberica-17-and-11-is-out/): Liberica 17 一 the new base for NIK 21.3 The new LTS release of Liberica JDK is the event that leads to many new releases of our products. We present the latest version of Liberica NIK, a tool based on GraalVM that accelerates your application by utilizing native image technology. It is based on Liberica JDK 17 or Liberica JDK 11, two of the latest LTS versions. Discover Liberica NIK Contents Why Liberica NIK? New version of GraalVM in Liberica NIK Spring Boot Native depends on Liberica JDK Enhanced desktop support Liberica NIK + AWT/Swing example project Liberica NIK + OpenJFX example project Open source software approach Conclusion 1. Why Liberica NIK? There are many reasons to choose Liberica NIK as a powerhouse for your native images! New version of GraalVM in Liberica NIK The Graal Virtual Machine that powers Liberica NIK has also been updated to the latest version that comes with an abundance of new features: Initial support for Java module system in native-image. The following command line options are supported: “--module”, “--module-path”, “--add-exports”, and “--add-opens”. They work the same way as in the Java launcher, so now it’s possible to natively compile an application that consists of several modules; You can now configure Reflection and JNI configuration file entries to apply only in the case when a particular class is reachable during analysis. This feature helps in producing smaller and faster native images; The new “adaptive” garbage collection policy helps to reduce memory footprint and to make GC more performant. It is now used by default. The previous policy “BySpaceAndTime” can be utilized by running the “-H:InitialCollectionPolicy” command line option; The native image generator can now read command line arguments from a file using the “@file” syntax the way javac does. Spring Boot Native depends on Liberica JDK Liberica JDK is the default runtime utilized with Spring applications, making Liberica NIK the best utility for producing Spring Boot native images. 2. Enhanced desktop support In this new update, we provide enhanced support for desktops. We introduce the new “Full” Liberica NIK flavor including OpenJFX. Liberica NIK now works with AWT/Swing (core, standard and full builds of Liberica NIK for Linux; based on Liberica JDK 11) and OpenJFX (only full builds for Linux and Windows; based on Liberica JDK 11 and 17). List of Liberica NIK 21.3 supported platforms Liberica JDK base for Liberica NIK + OS & CPU Headless(non-GUI) applications support AWT/Swing OpenJFX (in full builds only) Liberica JDK 11 + Windows x86 + - + Liberica JDK 11 + Linux x86 + + + Liberica JDK 11 + Linux ARM + - - Liberica JDK 11 + Alpine Linux x86 + - - Liberica JDK 11 + Alpine Linux ARM + - - Liberica JDK 11+ macOS x86 + - - Liberica JDK 17 + Windows x86 + - + Liberica JDK 17+ Linux x86 + - + Liberica JDK 17 + Linux ARM + - - Liberica JDK 17 + Alpine Linux x86 + - - Liberica JDK 17 + Alpine Linux ARM + - - Liberica JDK 17+ macOS x86 + - - We plan to add support for AWT/Swing in Liberica NIK for Windows and Mac and OpenJFX for Mac in the future releases. Note that the full support of OpenJFX has already been implemented for some time in full builds of Liberica JDK 8, 11, 17. Liberica NIK + AWT/Swing example project To display the Liberica NIK at work, we present how to convert a Swing application to a native image. In this example, we will use the Stylepad demo application. Remember, you need to use the Liberica NIK build based on Liberica JDK 11 for this to work! Before we start The first thing to remember is that to build a native image for a Swing application, you need to keep some details in mind: Graal sets java.awt.headless property to true by default. Don’t forget to explicitly set java.awt.headless property to false when generating a native image for swing applications; Explicitly provide resources used by the application such as property files and images to the native image tool; Do the same with classes used by reflection or serialization in the application: explicitly provide them to the native image tool. Getting the project ready Note that the Stylepad JDK application uses serialization. Run the Stylepad demo with Graal tracing agent to dump the serialization classes used by the demo: java -agentlib:native-image-agent=config-output-dir=conf-dir -jar Stylepad.jar Set java.awt.headless property to false. Then provide *.properties, *.gif resource files, and the serialization config file: native-image -Djava.awt.headless=false '-H:IncludeResources=.*/(Notepad|Stylepad).*properties$' '-H:IncludeResources=.*/.*gif$' -H:SerializationConfigurationFiles=conf-dir/serialization-config.json -jar Stylepad.jar Image taken from natively compiled Stylepad demo Building the native image Run the Stylepad demo with Graal tracing agent to dump the resources and reflection classes used by the demo: java -agentlib:native-image-agent=config-output-dir=conf-dir -jar Stylepad.jar Run the native image tool with resource and reflection configuration files that you just generated: native-image -Djava.awt.headless=false -H:ResourceConfigurationFiles=conf-dir/resource-config.json -H:ReflectionConfigurationFiles=conf-dir/reflect-config.json -jar Stylepad.jar Discussion Note that to make the NIK AOT compilation work with the project sources, we had to trace and update this project with: Classes used in reflection (Toolkit, FontManager, L&Fs, KeyEvent); Resources used by L&Fs. Swing classes used by reflection or JNI were added to the current version of Liberica NIK. They were obtained by using the tracing agent and run with several Swing applications. It is possible that some usages of reflection or JNI are missed. To solve this, you need to run the tracing agent and add the missed classes manually. Numeric results Size comparison jdeps --list-deps Stylepad.jar java.base java.desktop java.logging jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.desktop,java.logging --output jdk-desktop Size comparison of different kinds of Stylepad app packaging Starting Stylepad demo by jar file: time -v jdk-desktop/bin/java -jar Stylepad.jar User time (seconds): 1.89 System time (seconds): 0.09 Percent of CPU this job got: 17% Maximum resident set size (kbytes): 72192 Starting Stylepad native image: time -v ./Stylepad User time (seconds): 0.10 System time (seconds): 0.05 Percent of CPU this job got: 1% Maximum resident set size (kbytes): 71760 Known Limitations The current AWT/Swing support is implemented only on Linux OS and for Liberica NIK builds based on Liberica JDK 11; Gradients and borders are not correctly drawn on JButton for Nimbus L&F. Use other design solutions; There is the native image linker failure for applications that explicitly use the SplashScreen class. Future work In the subsequent releases, we plan to Add AWT/Swing support for Windows and macOS platforms; Resolve issues for applications utilizing a splash screen. Liberica NIK + OpenJFX example project Our second example involves using OpenJFX to build the native image of the “Brick Breaker” game. This solution works on both builds of Liberica NIK (based on Liberica JDK 11 or 17), and both supported OS: Windows and Linux. Building the native image First, download & install nik-openjdk11-full-21.3 from bell-sw.com. Run the command native-image -H:IncludeResources='ensemble.samples.shared-resources.brickImages.*' -jar BrickBreaker.jar brickbreaker To include the resources (such as images, sounds, or XML files) in the native image, execute the following line: -H:IncludeResources='com.fxapp.package.resources.*' To find all the available usages see the GraalVM documentation for more options. Then be patient and wait for the process to finish. Run ./brickbreaker Image taken from natively compiled BrickBreaker OpenJFX demo Discussion Performance results Size RSS Startup Liberica 11.0.13-full + BrickBreaker.jar 517M 587M 360ms Custom image (jlink --add-modules javafx.graphics) + BrickBreaker.jar 122M 422M 360ms Native image 33M 384M 270ms Limitations and how to overcome them Currently available for Windows and Linux x64 only; javafx.media and javafx.web modules are not supported at the moment; Reflective access to classes/methods/fields (including JNI) should be configured before running native-image. We handle reflective calls made by OpenJFX libraries for you, but you still should properly configure those made by your application. Future work macOS support is planned for 2022. Got any questions? Book a free consultation! Get Free Advice 3. Open source software approach BellSoft, the creators of Liberica JDK and Liberica NIK, follow the open source approach to development. We continuously push changes upstream to the mainline of OpenJDK. We participate on the GraalVM advisory board. This makes us experts on native image technology. Our software is preferred by companies that value the stability and security of runtime. The latest example is VMware who chose Liberica NIK as a fully supported end-to-end native solution for Spring Boot native images. Discover Liberica JDK 4. Conclusion The new Liberica NIK v21.3 is the reliable and tested solution for building native images with Spring, and now it works perfectly with desktop native images, bringing the stability, security, and support of Liberica JDK to Linux and Windows desktop apps. Bellsoft will continue further development of NIK with more features and supported systems to come in the future. Download Liberica NIK - [BellSoft releases Liberica Administration Center](https://bell-sw.com/announcements/2021/11/02/bellsoft-releases-liberica-administration-center/): Managing an extensive IT infrastructure is challenging. Imagine a company with thousands of PCs. Various Java applications have been piling up on them for years, JDK versions have been updated at different times, or never actually updated. Moreover, they all might stem from different vendors. Is it even possible to unravel this tangle and bring order to it? “Yes, it is,” say the BellSoft engineers. The only thing you need is a server-based system which has access to all computers in your company to gather statistics and remotely work with JDKs/JREs. And that is precisely the tool we have developed! Discover LAC Liberica Administration Center (LAC) is a software management system that helps you to monitor and control every Java Runtime within your corporate network from a single dashboard, update Java versions, or apply patches with a single click. This Java runtime management tool has a unique set of functions that you won’t encounter in any competitors. It: Collects data about Java runtimes (version and vendor) and applications in your Windows PC fleet (Windows 7 or higher); Notifies about potential security issues (CVEs, license control) and proposes the solutions to them; Automatically performs scheduled daily or monthly tasks; Uninstalls unnecessary JDKs/JREs; Updates Liberica JDK to a required version. Based on the characteristics above, we can assume that using LAC gives you several benefits: It saves you a tremendous amount of time and money, as your system administrators don’t have to manually extract data or install/update Java™ on each PC; You can easily tackle potential issues related to the usage of licensed versions (which may lead to unexpected significant expenses) or obsolete versions (without up-to-date security patches) of Java™; LAC provides you with an inventory of all Java apps launched on the PC. Based on this list, you can decide whether these apps are needed on the host. Although there are several software management systems on the market, LAC has a unique set of functions. Download product comparison to see what makes LAC the best solution for the enterprise. Download tools comparison It might seem that a tool performing such complicated tasks would require special operating skills. We dare say this isn’t true. Let’s break down LAC’s workflow to demonstrate that genius rests in simplicity. How to use LAC First, let us look at the process of communicating with the network of desktops. LAC agents get installed on all PCs (the Installation can be performed automatically). The agents automatically “wake up” every hour, gather information and, if configured to do so by the administrator, check with the server for updates and send the list of installed Java runtimes to the server. If there is a task from the server, the agents then receive and accomplish it on their PCs. The server-based part (the brain of LAC) has a beautiful, user-friendly interface: The Dashboard section represents the general statistics on all hosts: Security: shows JDKs/JREs that need to be updated or have reached the end of life stage; Subscription required: shows JDKs/JREs that have a license issue (i.e., versions that require a paid license but are still in use); JDK Vendors: lists JDKs by vendor; JDK Versions: lists JDKs by version. You can dig into the security issues and display a graph on common vulnerabilities and exposures affecting your hosts: As you can see, half of your problem is already solved: data on your whole PC fleet is gathered on a server within your organization without much effort on your part. However, LAC is not only about monitoring. Remember that the second part of the problem is to ensure uniformity of your fleet. For that purpose, LAC has a Tasks section: Here, you can create two kinds of tasks: Uninstall a JDK/JRE if it is no longer needed on the host; Update Liberica JDK. In addition, to save time and trouble, you can select one desktop or perform a task on multiple PCs at once. You can also automate the updates or schedule tasks and let LAC deal with them while you concentrate on other challenges at hand. Obviously, the functionality of Liberica Administration Center has no negative impact on the learning curve. You can start working with LAC immediately after installation, but if you still have questions, BellSoft technical support will gladly help you. Ready to give LAC a try? “That is all very well,” you may say, “but what about some real-life proof of efficiency?” We understand if you want to see LAC in action before adopting it into your standard workflow. For that purpose, we prepared a demo version of our software management system. Fill in the form below, and our engineers will guide you through the whole process of installing and running Liberica Administration Center in your company. Get a personalized demo - [Discover the world of evergreen software at JRush](https://bell-sw.com/events/2021/11/25/Events-JRush/): We are back with the third episode of JRush, a series of always-free web conferences where Java engineers get concise yet extensive information on a particular topic. This time, we will guide you towards a flexible, agile, self-updating, and cost-effective IT infrastructure. Join us online on Dec. 2, 11 am PT. Together we will explore tools and methods that will help you enhance the security and performance of your Java apps and leave behind the nightmare of big bang migrations and upgrades. Only renowned experts from the industry share their experience at JRush. This time, you will hear from: Adam Bien, Developer, Consultant, Trainer, Podcaster, and Java enthusiast; Dmitry Chuyko, BellSoft Senior Performance Engineer; Anton Arhipov, Kotlin team Developer Advocate at JetBrains. Spend just two hours and get the most valuable information and copy-and-paste examples: “Stay Evergreen: Write Once, Never Migrate”. In his talk, Adam Bien will share the methods of developing evergreen software to minimize migrations and to adopt lean code, incremental learning, and longer vacations. “Rock your VMs and containers. Introducing Alpaca Linux”. Dmitry Chuyko will introduce Alpaca Linux, a perfect Linux distribution for Java deployment. Alpaca is enhanced with a tuned kernel, modern security features, and optimized libc to boost performance. “Ktor — naturally powerful framework”. Ktor is a lightweight framework for building asynchronous applications. Anton Arhipov will show how to create Kotlin applications with WebSockets and a database using Ktor. Register now and see you at JRush! Register for JRush But we have more good news for you! We are happy to announce that from now on, the new JRush episodes will come out every quarter, so you will always stay on top of the cutting-edge trends and technologies in the Java industry. Knowledge is power, especially in IT, where new solutions constantly appear and become the ultimate game-changer. JRush conferences provide you with this power. If you’ve missed the previous JRush episodes, don’t worry. Subscribe for free and get access to all materials: watch the recordings of previous sessions, download the presentations, or join the life streams of new episodes. Subscribe for free - [DevOps: how to make it work for you](https://bell-sw.com/announcements/2021/12/01/devops-why-it-does-not-work-for-you-and-how-to-fix-this/): The concept of DevOps dates back to 2009, yet it remains a hot topic. Everybody wants a DevOps engineer in their company. People talk about DevOps practices and implement them into their processes. Still, many developers, when engaged in private conversation, say that DevOps does not work, does not help, and makes their job more complicated and even annoying. Is there a reason why DevOps keeps proving its efficiency in some companies, while becoming a nuisance in others? Let’s dive into the issue and find out. Contents What makes DevOps tick Top 10 common mistakes of practicing DevOps Using Scrum as the micromanaging tool Not using the united runtime environment Testing only “the happy path” and “the clean data” Overtesting Making the DevOps engineer a bottleneck Introducing “DevOps” without implementing Agile Ignoring the security Making your teams work on different goals Working with the generic tools only Making DevOps a routine without a purpose The biggest mistake - not utilizing DevOps What makes DevOps tick To discover why DevOps does work, let us explore the cases where it does not. It is easy to find many opinions about DevOps inefficiency, including these: This is what developers and even DevOps engineers say about DevOps When you read these opinions, you always find one common point: “What we work with is not DevOps, but an imitation.” This often happens when managers treat DevOps as a universal guide or even a ritual with no substance, rather than the philosophy or set of practices needed to be adjusted for different tasks. Every story about failed DevOps implementation usually involves decisions, where management does not really know what DevOps is or its goals before they start introducing it to the company’s software development workflow. This means we need a clear understanding of what DevOps is before finding out why it does not work in some cases. In BellSoft, we like the definition of Donovan Brown, a Partner program manager at the Azure CTO incubations team at Microsoft and a speaker at our own JRush conference. Donovan says that DevOps is the union of people, processes, and products to enable a continuous delivery of value to end-users. And the emphasis on “continuous” and “value” is the essential part. When working on implementing DevOps practices, managers sometimes forget the end goal, which is to deliver a valuable product. It all goes downhill from there. Developers lose themselves in the endless cycle of updates and software releases that do not make the customers happy. With every new version, the product devolves. So if there is one thing you take away from this article, it should be this 一 remember the end goal and implement the practices with this goal in mind! There is no single universal approach to DevOps that will work for every company. Invent your own processes, do not stick to the formal guidelines. Be creative, and always remember to bring a valuable product to your customers. Let us discuss the common mistakes developers still make in 2021 when implementing DevOps. Or contact our engineers and let them share their experience of DevOps in the world of Java development. Contact us Top 10 common mistakes of practicing DevOps Using Scrum as the micromanaging tool Scrum is an excellent practice for automation, one of the most important aspects of a successful DevOps routine. And yet, administrators tend to use it as the micromanaging tool, implementing only some values of Scrum while ignoring the others. Remember these values are not only Commitment and Focus, but also Openness, Respect, and Courage, and they only work together! If your Daily Scrum (which should be a 15 minute event) turns into an hour-long meeting, you are doing something wrong. If someone outside the Dev team decides who should work on which tasks, it is not Scrum. If Sprint reviews do not lead to future adaptations, then they are useless. If you only utilize Scrum to put your team into time brackets, you better avoid working with it at all. Not using the united runtime environment “It worked on my computer” is a phrase no one wants to hear, and to avoid hearing it, consider utilizing a JRush page to listen to full presentations. - [Vulnerability scanning for Java apps](https://bell-sw.com/announcements/2021/12/08/vulnerability-scanning-for-java-apps/): Better safe than sorry “Safety first” is the motto engineers at BellSoft follow in delivering our products to customers. As a leading OpenJDK contributor and a member of the OpenJDK Vulnerability Group, we cooperate with the community in hunting down and fixing security issues, and making sure that Liberica JDK is free from common vulnerabilities. Our support team is there for you 24x7x365 with response times as fast as one hour. Your runtime is the environment where you develop your product. Regardless of how safe the JDK distribution is, you should ensure the safety of your product throughout the whole delivery chain and eliminate security risks in a timely manner. This is where vulnerability assessment tools come into play. In this article, we will find out why vulnerability scanning is an indispensable component of DevOps strategy and compare two popular scanners for Java projects. Finally, we will learn how to scan the application for security issues using a scanner and Liberica JDK as a runtime. Vulnerability scanning is a must Scanners, your loyal watch dogs How to choose a suitable scanner Snyk Xray Jfrog Fortify your project with Liberica JDK and a vulnerability scanner Conclusion Vulnerability scanning is a must The beauty of the OpenJDK project is that you use the Java SE version and enjoy the open source freedoms brought to you by the GPL license. Just remember how many dependencies you integrate into your project during the development: dozens or even hundreds! Maven repository holds build artifacts and dependencies for every occasion. Whether you need to parse a file, connect to a database, or conveniently manipulate Java classes by means of additional utility methods, Maven offers it all and more. What is important, the repository holds different versions of dependencies so that you don’t encounter problems with compatibility. All in all, dependencies make the development process fast, convenient, and they are available for free due to their open source nature. But there is a flip side to the open source coin. As there are a lot of dependencies, and they all should be compatible with your JDK version or each other, there is always a risk of integrating an older version with known vulnerabilities. Those vulnerabilities do not cause errors in the work of your application, and so they sneak undetected into the next development stage. As a result, the customer will receive a product with security gaps. So continuous security monitoring must be an integral part of DevOps processes at your company. The safety of product components must be guaranteed at every stage of the CI/CD pipeline. Ideally, the developers must: Use artifacts only from reliable sources; Integrate only the latest dependency versions without known vulnerabilities; Monitor the appearance of new packages with fixed vulnerabilities; Update the dependencies as soon as new versions become available. Luckily, this process can and should be automated and enhanced through vulnerability scanning. Scanners are vulnerability testing tools. They are doing a great job at revealing weak spots of the code, leaving no stone unturned. Besides vulnerability scanning, you should also update your runtime as newer versions contain fixes for known bugs and common vulnerabilities. BellSoft provides quarterly CPUs (critical patch updates) in addition to feature releases, which come out twice a year. This way, you don’t have to worry about stability and security of your runtime and focus on fortifying your own code. Find out how below. Scanners, your loyal watch dogs The operational principle of vulnerability scanners is similar to that of antivirus systems. When vulnerabilities are revealed, they get fixed in newer component versions. At the same time, they are put into a database. A scanner interacts with the database when reading the code and matches the scanning results with the available data. At the end of the assessment, it generates a log file listing the revealed weak spots with assigned severity ratings and possible remediations. Vulnerability scanning process The job of the scanner doesn’t end there, though. Although it is integrated into the project as early as the build stage, it monitors the safety of components across the CI/CD pipeline. Some of them have the function of continuous post-production monitoring. After the issues are identified, there are two possible solutions: Update the dependency version. This may require some code rewriting for compatibility’s sake. Delete the dependency and use a component closest to your need. This might require A LOT of code rewriting based on how deeply the dependency is integrated. But in most cases, this extreme measure won’t be necessary since identified vulnerabilities are quickly fixed in a newer package. You don’t have to worry about obsolete versions of your software and compare versions yourself. The scanner does it for you. How to choose a suitable scanner There is a wide range of code security scanning tools available. The developer responsible for product release chooses a scanner based on your application type and programming language. The most popular scanners for Java development are Snyk, Xray Jfrog, and Black Duck. If you are looking for open source tools, there are open source vulnerability scanners such as SonarQube or Trivy. All scanners differ in terms of functionality and pricing. Let us compare two of them: Snyk and Xray Jfrog. Snyk Snyk is a developer security platform that scans the code, dependencies, containers, and infrastructure as code for vulnerabilities. It functions on four levels: Finds issues in the code in your IDE, gives remediation advice, and verifies the corrections; Integrates the source code repositories to scan for issues and prioritize them automatically. You can generate a detailed report on found vulnerabilities or fix multiple issues at once; Scans your containers for issues and continuously monitors container images throughout their lifecycle; Integrates with your CI/CD tool so that you can view the results of scanning and remediations without leaving the build tool. Snyk has its own security intelligence database, including public sources, data from proprietary research, community contributions, and machine learning mechanisms for continuous updates of security threats. It offers flexible subscription plans depending on the number of developers on the team. It also has a Forever free plan for individual developers striving to secure their build processes. Xray Jfrog Xray Jfrog is a binary analysis platform that protects the app across the CI/CD pipeline starting from IDE to the finished product, and provides continuous monitoring post-production. What distinguishes JFrog from similar tools is that it natively integrates with Artifactory and provides detailed information about security and compliance issues. JFrog uses deep recursive scanning of binaries, which means it can scan all underlying layers and dependencies, including those packaged in Docker images or zip files. In addition, JFrog creates a graph of the app; based on this graph, the tool gains full visibility and can determine the severity of impact. JFrog uses vulnerability intelligence VulnDB as a source of information about known security issues plus other metadata sources. As far as subscription plan is concerned, there are two options: Cloud. JFrog manages the infrastructure with automatic updates and guaranteed uptime; Self-hosted. You can maintain the tool on your hardware or in the cloud yourself. The tool offers support for on-premise, cloud, multi-cloud, or hybrid deployments. Regardless of the scanner you choose, the benefits are clear: Automated monitoring of your app at all stages of production with severity scaling and possible solutions; Acceleration of development thanks to the absence of manual scanning; Constantly updated data about known vulnerabilities ensures there are no security gaps in your product. Fortify your project with Liberica JDK and a vulnerability scanner Let us now see a vulnerability scanner in action. For that purpose, we will analyze a simple application with several dependencies using Liberica JDK as the runtime environment and Snyk as a scanning tool. Download and install Liberica JDK 17, the latest LTS version. Liberica JDK is a modern TCK-verified Java Runtime with the widest range of supported platforms and High-Powered support. You can choose any version you like, BellSoft provides support even for Java 6 & 7, but the latest versions have new features that may be helpful in your development process. Discover Liberica JDK Now we need a simple application with several dependencies. Download the Spring Petclinic sample project. You don’t have to add anything manually because the Petclinic project already contains a set of necessary dependencies. It’s time to set the scanner loose! You have several options of connecting Snyk to your applications: Source control (GitHub, Azure Repos, etc.); Container registries or Kubernetes; Continuous integration tools (CLI, Jenkins, etc.); IDE plugins (Eclipse, IntelliJ, Android Studio, etc.); Package repos or serverless (Artifactory plugin, AWS Lambda, etc.) We have chosen a plugin for IntelliJ IDEA. You can install it directly in your IDE through Preferences -> Plugins. After the installation, click on the Snyk icon to scan your project. As a result, Snyk discovered three vulnerabilities with different severity ratings: critical, high, and medium. You can click on the vulnerability and read detailed information with overview and remediation. Scan Report. Vulnerability of medium severity Scan Report. Vulnerability of high severity Scan Report. Vulnerability of critical severity Note, that a critical vulnerability hasn’t been fixed in a newer version yet. This is the case when you have to use another dependency to strengthen your code. Conclusion As you can see, integrating a vulnerability scanner into your project couldn’t be more straightforward. In return, you get all the benefits of modern protection and you meet the requirements of DevOps processes at your organization: continuous and automated monitoring of a product at all stages of production. With Liberica JDK and vulnerability scanning, you can be sure that your apps are safe and sound at all times. And remember, there is no such thing as excess when it comes to security! - [How to make Java the DevOps best friend](https://bell-sw.com/announcements/2021/12/16/how-to-make-java-the-devops-best-friend/): A successful DevOps strategy is not about abstract ideas, team collaboration, and good practices of continuous application delivery alone. It is also about technologies that make those practices come to life. For example, vulnerability scanners provide continuous safety monitoring throughout the CI/CD pipeline. Docker helps developers package, deploy, replicate, and shift containers in a streamlined way. Kubernetes enables automatic management of the containerized applications. In addition to that, there is a runtime. The development platform is a crucial component that influences the security and performance of the application and even the efficiency of your internal DevOps process for Java development. Let us find out how! This article will discuss how to organize a reliable CD pipeline by choosing the Java Development Kit distribution that best fits your DevOps methodology. You will also see that the simplest method for installation is not always the best one; it may be fraught with potential issues. Finally, we will discover a novel tool that will help you manage the runtimes of the whole Windows PC fleet with a single application. Alternatively, contact our engineers, and they will tell you how to boost the efficiency of your Java DevOps processes through top-class JDK distribution and cutting edge tools. Book a free consultation JDK distribution fitting the DevOps purposes Installing a JDK distribution: approaches and pitfalls Simple download is not that simple Install a package Use the developer’s website Containers APIs DevOps and the tools for automated Java monitoring Conclusion JDK distribution fitting the DevOps purposes When Oracle stopped releasing free builds of Java SE 8 in 2019, many companies started to look for alternatives. Luckily, Java was open-sourced long ago, so there are a few high-quality OpenJDK distributions to choose from. The question is, how to select a distro most suitable for your organization? A good Java Development Kit distribution aiding rather than hindering your DevOps processes must meet the following requirements: You will be unpleasantly surprised when your programs start behaving inappropriately after the migration. It means that the distribution doesn’t conform to the standards. TCK verification guarantees the compliance with Java SE specifications, so the migration can be performed without any issues. Timely updates ensure the security of your runtime at all times. The updates must be integrated continuously without delays, so there will be no chance left for exploits. A good Java Development Kit distribution has a release cycle aligned with Oracle’s plus quarterly CPUs with fixes to known vulnerabilities and bugs. Using different distributions for PCs, servers, and the cloud is expensive and time-consuming. A company has to monitor and integrate updates for each runtime individually. A unified runtime with a wide range of supported platforms significantly simplifies the process of testing, updating, security patches integration, and deployment. The DevOps processes in Java development must run smoothly and continuously. If a problem arises, it may impede and disturb the workflow, especially if you have to wait for days or weeks for it to be solved. A runtime with high-quality support enables almost immediate response based on SLA and without a middle man. For instance, a critical vulnerability in Java logging libraries has recently been discovered. Although the affected library is not part of Liberica JDK, we still can help our clients by fixing the issue because their security is our top priority. Convenient support conditions are also important. Some vendors offer pricing based on vCores. As a result, upgrading/downgrading your fleet will necessitate legally changing the contract, which demands money and precious time. With flexible per-instance pricing, you won’t encounter this issue. Besides, it is less expensive because the number of CPUs and the processor class do not matter. A company with a fleet consisting of thousands of PCs would benefit more from long-term support. After all, upgrading every two years is inconvenient and significantly slows down the development process. Therefore, a distribution with LTS-releases supported for at least eight years will enable your developers to concentrate on delivering a high-quality product to the customers rather than rewriting the code constantly. In the case of cloud development, time and size mean money. The smaller your containers are, the faster is the deployment and the fewer resources are consumed. A perfect JDK distribution should be fit for cloud use and provide small containers. Additional Java tools for DevOps would be a bonus. For example, Liberica JDK comes with Liberica Native Kit to accelerate the startup times and minimize static footprint. As you can see, there are a lot of factors to consider when selecting a JDK distribution. However, with the right choice, you will be able to automate your processes even more, reduce your TCO, speed up the development, and enhance the security of your application. Still not sure whether you should switch from Oracle JDK to OpenJDK? Download a comprehensive white paper dispelling common fears about OpenJDK and giving reasons for migration. Why switch from Oracle JDK to Liberica JDK Installing a JDK distribution: approaches and pitfalls DevOps powered by Java is a tempting idea. But before you enjoy the benefits of a selected JDK distribution, you have to install it first, which is not as easy as it seems. Simple download is not that simple OpenJDK is an open source implementation of Java SE, meaning that all components are free for download and use. So all you have to do is browse through all OpenJDK distros available, select a suitable one and download the necessary files. Then you can update your JDK by receiving and integrating security patches and updates. The process is simple, but in reality, there are a lot of variables you have to consider. First, you need to know the version you’re running and how it complies with the files you download. Then you must ensure that the chosen distro is TCK-validated to comply with Java SE standards. It would also help to run your own tests to see how the JDK works with your application and whether it satisfies your goals. Install a package Install a JDK package through a package manager, a tool that enables automated installation/deinstallation and runtime updates. This approach is more sophisticated than the previous one. There are a variety of package managers available on the market: OS-specific ones like YUM or APT for Linux, Homebrew for macOS, and cross-platform ones like SDKMAN!. But this approach has its downsides, too. Firstly, these managers must be installed on every machine, and JDKs are updated independently on each desktop. Secondly, you don’t have complete control over the updates cycle. The schedule depends on the distribution vendor. Security patches are published by OpenJDK four times a year, and each vendor then uses them in their builds. However, the builds may not be updated in a timely manner. So there’s a risk that you will install a distribution without the latest security fixes, and your application will be vulnerable to exploits. Use the developer’s website If you have found a JDK vendor meeting your requirements and the requirements to a secure and performant distribution, you can go directly to the vendor’s website and download the package from there. However, there are also a few things to consider. For example, a distribution may come in different flavors. In the case of Liberica JDK, these are Full, Standard, and Lite, each fit for specific purposes. Then you have to choose the installation method: run the installer or use a command line. Note that it’s essential to have a notarized installer that can also be used to uninstall the JDK because it may be more complicated with the command line. Containers Containers are the new black in the IT world. They are beneficial for Java DevOps development as they enable companies to build microservices and increase the agility, speed of deployment and updates. Packaging and delivery processes can be standardized and transferred across environments. But there are no two containers alike. They have different sizes depending on the OS and components, significantly influencing the pull time and memory consumption. Let us take a Liberica JDK container based on CentOS. Even optimized, it weighs 319 MB, so pulling the image will take 17 seconds over 100 Mbps network! If this is too much for you, opt for a smaller container. For example, Alpine Linux is a lightweight OS perfect for cloud deployments and containerized applications. Try out BellSoft’s generic Liberica JDK Alpine musl base image, which takes up less than 100 MB on the disk. A container based on Alpine musl and Liberica can be as small as 43.5 MB — it is the smallest container on the market! Another aspect worth paying attention to is a base image for the container. Sometimes you won’t have features you expect to be there or you’ll have a very outdated Java. If you don’t know what a current JDK version is, you can easily get a container one year outdated in terms of security. APIs Suppose you want to gain comprehensive information on a distribution before installing it. You can do that by means of Discovery APIs providing the full metadata on JDK binaries. APIs are meant to automate the process of notifications about JDK releases and security updates by consuming metadata. BellSoft uses its own technologies to create APIs for Liberica JDK. Our APIs are designed so thoroughly that we still provide all the metadata structured as v1.0, even for new products and features. We provide information about the supported architectures and operating systems, artifact types, versions, etc. You also get a download link and a checksum. In addition, our download links and APIs are persistent, meaning that they haven’t changed since the first version. To use the APIs, you can invoke the whole metadata text in JSON format or make multiple calls getting small pieces of data. The response will be much slower if you request the entire metadata for all the releases and platforms, so use filters and select fields to speed up the process. Finally, APIs play an essential role in JDK installation for Ansible. The Ansible role for Liberica JDK is cross-platform and can be used with Windows and Linux. DevOps and the tools for automated Java monitoring Earlier, we mentioned that a unified runtime simplifies testing and deployment. You are free to work with different vendors, but we believe it would be better to let one reliable vendor take care of your runtime security and updates. In addition, a unified runtime comes with useful utilities to automate your processes, reduce costs, and facilitate the development. For example, BellSoft has created a solution enabling full control of all the corporate runtimes from one window: Liberica Administration Center (LAC). LAC is a server-based application, which Collects data about Java runtimes (vendor and version) and applications in the Windows PC fleet; Notifies about security issues and license violations and offers a solution; Upgrades Liberica JDK or uninstalls JDKs/JREs; Performs daily and monthly scheduled tasks. LAC is indispensable for Java DevOps processes as it enables the company to automate runtime monitoring and updates. You will always stay on top of the latest Java improvements and be fully protected thanks to prompt and automatic installation of security patches and updates. In addition, you can upgrade the whole fleet simultaneously or each instance independently. This way, you will save a tremendous amount of time and money as your team won’t have to control the runtimes manually. Consequently, they will dedicate the time to other pending tasks. Therefore, the processes in your CI/CD pipeline will run as smoothly as possible. Conclusion In this article, we have seen a direct link between the Java Development Kit quality and DevOps efficiency. We described several methods of JDK installation: all of them have their pros and cons, so utilize the one that fits your purposes. We have also discovered a Java inventory and monitoring tool for DevOps that will keep runtimes in your PC fleet always secure and up-to-date. Java keeps gaining popularity, and not without reason. It is not only a language; it is a whole ecosystem with frameworks, tools, and a strong community. As a result, Java improves in line with modern requirements for cloud computing, containerization, process automation, and continuous software delivery. So use Java to boost the DevOps at your company, and you will not be disappointed! - [BellSoft annual results 2021](https://bell-sw.com/announcements/2021/12/24/bellsoft-annual-results-2021/): 2021 has seen the gradual recovery of the global economy from the pandemic crisis. The challenges the world still faces are immense, but so are the possibilities. We at BellSoft are proud to say that we did our best to do our part 一 by providing a better Java experience for all developers! We worked hard to deliver better cloud deployment, more secure development, and a faster time to market. The efforts we made have borne fruit, and 2021 became one of the most prolific years for our company: We delivered 100% sales growth compared to 2020; The number of Liberica JDK downloads has increased by 300 percent; More than 10 million developers learned about the company and our products. BellSoft’s 2021 in numbers This would have been impossible without you 一 our customers, partners, and users who put their trust in us and our products. We want to thank you wholeheartedly and assure you again that the security and convenience of your development is our top priority. Let us share with you what we did and plan to continue doing to reach our goal of enhancing the Java experience for everyone. We improve Java technologies As global trends in the IT industry shifted towards unified infrastructure, we focused our endeavors on delivering a full stack of highly performant, secure, and innovative technologies. Our team worked tirelessly to add four new products to our portfolio and enhance our main product, Liberica JDK, with the purpose of providing a better cloud and development experience: As far as Liberica JDK is concerned, we Released Liberica JDK 17, the LTS version, in line with the Java release cadence; Improved the SDKMAN! support; Added Apple M1, Aarch64, and AWS support; Reduced compilation time; Decreased the RAM footprint for containers; Added virtual machines and images for Kubernetes in YC, AWS, GCP, Azure; Maximized out of the box security with decreased number of CVEs, public CVE tracker, and safer defaults. Get the white paper on Liberica JDK We created Liberica Native Image Kit (NIK), a tool for accelerating application startup time and minimizing memory consumption. Our team continues working on further optimization of NIK performance by Adding Apple M1 support, as well as Mac/Windows support for AWT/Swing applications; Improving testing on currently supported platforms; Integrating a parallel GC. We now provide Liberica JDK for Embedded, which combines full Java functionality with minimal VM support and enhanced performance on the low-performing system components for the development of embedded devices. Further optimizations include improved JFX and added RISC-V support. The demand for a unified stack of technologies provided by one vendor goes hand in hand with the necessity to automate the processes of infrastructure management. We created a solution that meets both goals, Liberica Administration Center (LAC). LAC is a server-based system that provides automatic monitoring, license control, and security updates of the whole Windows PC fleet from a single dashboard. Get the white paper on Liberica LAC As far as the last product is concerned, some of our clients already know what it is. The others will soon find out as it is going to be something absolutely amazing! Don’t miss out on the news: join our community and subscribe to our newsletter. Subscribe to our newsletter BellSoft’s product portfolio Taken together, our products provide an end-to-end solution for enterprises, so you can enjoy all the benefits brought by a unified Java experience: Create and deploy small and fast containers thus reducing cloud expenses and accelerating time to market; Utilize a highly secure OS for convenient deployment; Use a unified runtime for any device, server, or cloud; Automate the installation, security updates, and license control of Java runtimes in a Windows fleet of any size; Receive High-Powered Support from one vendor with response based on SLA within 24 hours and without a middle man. If you would like to learn more about BellSoft’s product, contact our engineers. They will be happy to tell you how to transform your whole infrastructure and bring the development process at your company to a new level. Get Free Advice We strengthen the ties with our partners Alongside the development of new solutions and contact base expansion, we continue fostering collaboration with our partners. VMWare Tanzu has appreciated the quality of our runtime and the reliability of our support and has chosen Liberica Native Image Kit as a tool for Spring Native. As a result, VMware Tanzu customers will receive end-to-end native support via Liberica Native Image Kit and will be able to produce seamless Spring Boot native applications. We expand Java knowledge base BellSoft has always been a people-oriented company. We like to share our knowledge and experience with you, the developers and the Java community. As a result, our blog was replenished with 40 new articles dedicated to various technical topics. We aim to help any Java engineer who wishes to deepen his or her knowledge of the Java language or find a solution to the particular issue. We discuss the latest Java trends with you! You, our customers, form the latest trends in the IT industry, and to discuss them with you we created a series of quarterly web conferences for Java engineers called JRush. JRush has a very convenient format: 100 minutes; 3 experts; Q&A session. Only renowned experts from the industry talk about the newest technologies and the best development practices. We have already launched three episodes: All about native image, where we discovered the benefits of the native image technology and learned how to use it; DevOps philosophy: vision of the future, where we discussed the methods of increasing the performance of CI/CD pipeline; The evergreen evolution of Java, where we explored such tools as the Ktor framework, MicroProfile, and Alpaca Linux with a goal to transform the applications into evergreen software. JRush is a great opportunity to get a grip on the cutting-edge tools and get copy-and-paste examples of code, which could be utilized in your application. Moreover, JRush is free and will always be free because our aim is to raise awareness about the latest trends and arm the developers with the best development practices for the sake of building a common future. And our initiative has resonated well with the community. So far, JRush web conferences have been attended by three thousand participants from all over the world! Subscribe for free Today, as we stand on the eve of 2022, we reaffirm our goal to create the ultimate Java experience and thus make Java the Number one choice for enterprise. We help companies create modern, secure, and efficient solutions for global development. We will continue providing high-quality support and creating innovative tools in line with the global needs to make Java more convenient and efficient. Once again, we express our sincere gratitude to you 一 our partners, customers, the OpenJDK community, and developers who use our products. Only through common effort can we overcome the challenges of today and build a more sustainable and secure world through technologies. See you in 2022! - [Liberica 17.0.2, 11.0.14, and 8u322 builds are out](https://bell-sw.com/announcements/2022/01/19/liberica-17-0-2-11-0-14-and-8u322-builds-are-out/): We are happy to announce that today we release Liberica JDK versions 17.0.2, 11.0.14, and 8u322 as part of the quarterly CPU cadence. CPU (Critical Patch Update) releases help keep the runtime secure and performant as they contain CVE and bug fixes, whereas PSU releases contain non-critical fixes. The release contains 790 fixes and backports. 9 security issues were fixed with the participation of BellSoft (8 in JDK and 1 in FX). Contents How to keep your runtime secure The summary of fixes List of security issues fixed Summary of fixes in Liberica JDK Notable upstream changes Supported platforms Enjoy the most stable runtime! Useful links How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updated and patches are available at no cost. Download Liberica JDK The summary of fixes 16 security issues (CVEs) fixed; 93 total security fixes in CPU release: in Liberica 8u321: 30 security fixes (28 + 2 in FX), in Liberica 11.0.13.0.1: 33 security fixes (31 + 2 in FX), in Liberica 17.0.1.0.1: 30 security fixes (28 + 2 in FX). In addition, PSU releases include a total of 790 bugs and backports fixed: in Liberica 8u322: 30 security fixes (28 + 2 in FX) + 52 additional fixes, in Liberica 11.0.14: 33 security fixes (31 + 2 in FX) + 353 additional fixes, in Liberica 17.0.2: 30 security fixes (28 + 2 in FX) + 292 additional fixes. Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2022-21341 5.3 core-libs java.io:serialization network low none none unchanged none none low CVE-2022-21365 5.3 client-libs javax.imageio network low none none unchanged none none low CVE-2022-21282 5.3 xml jaxp network low none none unchanged low none none CVE-2022-21291 5.3 hotspot runtime network low none none unchanged none low none CVE-2022-21277 5.3 client-libs javax.imageio network low none none unchanged none none low CVE-2022-21305 5.3 hotspot compiler network low none none unchanged none low none CVE-2022-21299 5.3 xml jaxp network low none none unchanged none none low CVE-2022-21296 5.3 xml jaxp network low none none unchanged low none none CVE-2022-21349 5.3 client-libs 2d network low none none unchanged none none low CVE-2022-21283 5.3 core-libs java.util network low none none unchanged none none low CVE-2022-21340 5.3 security-libs java.security network low none none unchanged none none low CVE-2022-21293 5.3 core-libs java.lang network low none none unchanged none none low CVE-2022-21294 5.3 core-libs java.util network low none none unchanged none none low CVE-2022-21360 5.3 client-libs javax.imageio network low none none unchanged none none low CVE-2022-21366 5.3 client-libs javax.imageio network low none none unchanged none none low CVE-2022-21248 3.7 core-libs java.io:serialization network high none none unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: 17.0.2: 15 (15 + 0 in FX); 11.0.14: 15 (15 + 0 in FX); 8u322: 13 (13 + 0 in FX). Notable upstream changes Updated Timezone Data to 2021e The issue is related to the time zone rules, which were updated by the IANA Time Zone Database in 2021. Some changes introduced in 2021b caused compatibility problems and contained several typos. The issue is resolved by updating to the 2021e release. Corrected Files.walkFileTree method In cases where a zip file contained the directory with “.” name, the Files.walkFileTree would walk infinitely. A bug causing a similar problem with “/” in the directory was fixed earlier. The solution to this issue is to reject the zip files with “.” and “..” in name elements to be utilized as a file system. When the java.nio.file.FileSystems.newFileSystem(...) methods are invoked, such folders make them return the ZipException. Fixed accidental cleaning of valid megamorphic vtable inline cache by GC The issue was related to the GC behavior. A 10-year-old bug causing long “Concurrent Process Non-Strong References” times with ZGC (Z Garbage Collector) could trigger major latency and throughput issues for applications. In summary, the GC cleaned the megamorphic vtable call cache in some cases (see detailed description in Java Bug System) and after that, Java threads corrected the cleaned caches using ICStubs. Those caches then became megamorphic vtable calls again. As a result, the GC and Java threads continuously changed the inline cache between clean and megamorphic vtable calls and scheduled ICBufferFull safepoints. This could last for many seconds or several minutes. To verify that the issue was fixed, examine the logs after running with -Xlog:gc* safepoint. Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Yandex Cloud Enjoy the most stable runtime! BellSoft is committed to providing developers with the utmost Java experience. Therefore, we work constantly on improving the security and performance of our products, as well as keeping your runtime safe at all times. The new Liberica JDK builds are available for download! Click here or on the button below to head over to Liberica Download Center. Download Liberica JDK Useful links [JDK-8275766] Update Timezone Data to 2021e - Java Bug System [JDK-8251329] Files.walkFileTree issue - Java Bug System [JDK-8277212] GC issue - Java Bug System - [Java 18 一 many new features in a non-LTS release](https://bell-sw.com/announcements/2022/01/27/java-18-many-new-features-in-a-non-lts-release/): Contents Overview New features JEP 400: UTF-8 by Default JEP 408: Simple Web Server JEP 413: Code Snippets in Java API Documentation JEP 416: Reimplement Core Reflection with Method Handles JEP 417: Vector API (Third Incubator) JEP 418: Internet-Address Resolution SPI JEP 419: Foreign Function & Memory API (Second Incubator) JEP 420: Pattern Matching for switch (Second Preview) JEP 421: Deprecate Finalization for Removal Conclusion Overview Java 18 is released and available for everyone! Our instance of Java 18, Liberica JDK 18, is ready for download. It is not an LTS release, so the changes and enhancements are not as huge as in last year’s Java 17, and yet, there are some interesting new features! In the previous release, there were a lot of functionalities deprecated for removal or removed, but this year, there are not only upgrades to the existing JEPs, but some completely new enhancements. What is interesting is that some of those enhancements were functioning at the time when Java 17 came out, but they required additional testing, so in case you upgrade to Java 18, you will see them enhanced and stabilized. This is another example of how Java™ is being constantly tested and upgraded with every release by many participants of this open source project, including BellSoft. If you have any questions, our engineers are glad to provide you with all the information you need to decide if you are ready to upgrade. Get free advice So without further ado, let us explore the new features of Java 18! New features JEP 400: UTF-8 by Default A lot of APIs (including the standard Java APIs) use default charset in the applications, but what exactly is a default charset? Before it was environment-dependent, meaning that it could be different, as an example, for Windows, Linux, and Mac users. So the same application (even launched on the machine) by different users could corrupt text written in some languages (especially logographic ones). To fix this issue, a decision was made to use the most wide-spread charset, the UTF-8. The potential problem is that new issues may arise after migrating to Java 18 from earlier versions. If the source files were encoded with UTF-16, the javac compiler is affected because it assumes that .java source files are encoded with the default charset unless configured otherwise. So this should be noted when migrating from earlier versions of Java™. But as you can still set other charsets as default manually, no huge problems are expected with this change, and it will ensure more stable work in the future. JEP 408: Simple Web Server This JEP adds the command to run the web server with minimal functionality, loopback address, and port 8000 intended for testing and learning. The files are served from the current directory. The command itself is $ jwebserver In case of the successful startup, jwebserver prints a message to System.out with the list of local addresses and the absolute path of the directory being served. The server is customizable and enhanced with new APIs for server creation and customized request handling. The new classes are built on the existing classes and interfaces in the com.sun.net.httpserver package. The goal of this out-of-the-box solution is to provide means for easy testing, prototyping, and training. JEP 413: Code Snippets in Java API Documentation This change could be perceived as small, but for some developers it is the most important and exciting one of the whole release! It introduces an @snippet tag for JavaDoc’s Standard Doclet, that simplifies the inclusion of example source code in API documentation. It not only marks up the code included in the comments, but also validates it the same way as it does the main code, drastically reducing the chance of human error. This enhancement is full of features like compatibility with both internal and external snippets , text highlighting, and linking the text to the declarations elsewhere in the API. All in all, this feature will make the developer’s life much easier when they have to dig into other people’s code. JEP 416: Reimplement Core Reflection with Method Handles java.lang.reflect API is now reimplemented using the method handles, replacing the bytecode-generating implementations of Method::invoke, Constructor::newInstance, Field::get, and Field::set. The older VM’s native reflection mechanism is used only during early VM startup, before the method-handle mechanism is initialized. This enhancement will lessen the usage of unsafe APIs in Core Reflection to the minimum and also allow the better utilization of new Java™ features such as those introduced in Project Valhalla. JEP 417: Vector API (Third Incubator) The Vector API introduced in Java 16 can in some cases (like financial operations, machine learning, and cryptography) greatly boost the performance. It helps writing complex vector algorithms in Java™ in case, when using the existing HotSpot auto-vectorizer is not enough to achieve the complex computation, and you need to utilize the user model of vectorization. The third incubation is built in accordance with community feedback and adds some new functionality, with two most notable enhancements: support for the ARM Scalar Vector Extension (SVE) platform, and improved performance of vector operations that accept masks on architectures that support masking in hardware. We already provided an example of how Vector API impacts the performance at the time of the second incubation, and proved it to be a working solution. JEP 418: Internet-Address Resolution SPI To resolve the host names to internet protocol (IP), Java™ uses the java.net.InetAddress API, which in turn utilizes the OS’s resolver. Usually it works with the combination of local hosts file and the DNS (Domain Name System). This JEP introduces the service-provider interface so the java.net.InetAddress API could use alternative resolvers. This is done with several goals in mind, including the testing purposes and customization capabilities. And last but not the least, an alternative resolver could implement the DNS client protocol directly, without it being blocked on the OS-level, which allows, for example, the unrestricted utilization of Project Loom, that can now use multiple virtual threads to process the network requests. The new methods for the InetAddress API allow to manually include the implementation that will work without locking and use a service loader to locate a resolver provider. JEP 419: Foreign Function & Memory API (Second Incubator) This is another upgrade of the feature introduced in an earlier release to replace the Java Native Interface (JNI) with a superior, pure Java™ development model. It allows the usage of native libraries, foreign functions, and accessing the foreign memory with boosted performance. In the second incubator stage, the new carriers are implemented (boolean, MemoryAddress), with a simpler API to obtain downcall method handles and manage temporal dependencies between resource scopes. A new API for copying Java arrays to and from memory segments is introduced. JEP 420: Pattern Matching for switch (Second Preview) And yet another update of the previously implemented feature. It allows an expression or statement to be tested against several patterns, each with a specific action. Delivered before as the preview feature, it now provides two new enhancements. The first one is that dominance checking now forces a constant case label to appear before a guarded pattern of the same type. It makes it possible for the selector expression to match multiple labels in a switch block. Consider this problematic example: static void error(Object o) { switch(o) { case CharSequence cs -> System.out.println("A sequence of length " + cs.length()); case String s -> // Error - pattern is dominated by previous pattern System.out.println("A string: " + s); It is a compile-time error if a label in a switch block is dominated by an earlier label in that switch block. This means that you can check for bad code in similar situations. \ The second enhancement improves the exhaustiveness checking of switch blocks, making it more precise with sealed hierarchies where the permitted direct subclass only extends an instantiation of the (generic) sealed superclass. In other words, the compiler now not only helps keep your code organized, but now fully understands sealed classes hierarchies, which is another improvement in accordance with Java™ ideology. If you are interested, you really should check out the description of this JEP, as it is not possible to dive deep into this issue without making this article bloated. JEP 421: Deprecate Finalization for Removal The only deprecated feature in this release, this JEP sets the Finalization, that was introduced as early as in Java 1.0, for removal and was a constant source of headache for developers for more than 20 years because of the way it was implemented. To understand the issue we need to get into the way that Java’s built-in automatic memory management works. There are Garbage Collectors that free memory and this happens behind the scenes. They check for objects that are unreachable. And when the garbage collector decides that they are no longer needed it frees their memory. The problem is that some of these objects represent resources provided by the operating system such as an open file descriptor or block of native memory. For them reclaiming heap memory is not enough, the program has to release the underlying resource back to the operating system. So in case it does not happen, the resources are leaked - they are still considered in use by the OS and therefore are not freed. As the application works the number of such leaks grows and they create vulnerabilities or just crash the program. The finalization was invented as a way to get rid of leaks, but unfortunately it did not work that great because of unpredictable and unconstrained behavior. Not recommended for use for some time, now it is deprecated for removal. The process will take some time, because this function is used in many applications and standard Java API. Alternative mechanisms are try-with-resources statement introduced in Java 7 and explicit Cleaners introduced in Java 9. But the transition process could take a long time, as many libraries and even JDK itself utilize finalizers profusely. Conclusion The good news is that Java 18 does not only enhance the previously implemented features, but introduces a surprisingly vast amount of innovations. It seems that the reasons to migrate to the latest version keep arising, and with Java 8 being almost 8 years old, now is the good time to start considering updating to the latest LTS release. And while even Java 6 and 7 are still supported by BellSoft, the release of Java 18 provides a lot of motivation to at least test it, possibly by starting the new project with BellSoft tiny Docker containers. Download Liberica JDK - [Liberica Native Image Kit 22.0.0.2 and 21.3.1 builds are released](https://bell-sw.com/announcements/2022/01/28/liberica-native-image-kit-22-0-0-2-and-21-3-1-builds-are-released/): We are happy to announce that the new Liberica Native Image Kit (NIK) 22.0.0.2 is now generally available. In addition, we improved the previous version and released NIK 21.3.1. We will provide security update releases for the NIK-21.3 branch throughout 2022. We plan to provide similar support for NIK-22.3 throughout 2023. These update releases will occur four times a year and will be synchronized with JDK update releases. Now, let’s see what’s inside the new builds! Why you should use Liberica NIK If you are only getting acquainted with native image technology, let us first explain what Liberica Native Kit is all about and how you can benefit from integrating it into your project. Liberica NIK is a GraalVM-based technology that converts a JVM-based application into a native executable, which contains the application, dependencies, and runtime components to run the app on any supported platform without installing the runtime. NIK provides: Almost instant startup (1/10 second); Optimized resource consumption; Minimal static footprint; Opportunity to create multilingual microservices thanks to a wide range of supported platforms and programming languages; Tiny containers running at high speed, thus accelerating deployment and reducing cloud expenses; Support for OpenJFX (full builds of NIK only). In addition, Liberica NIK is based on LTS JDK versions, so you’ll get 8-year access to bug fixes and improvements. Summary of fixes and improvements Liberica NIK 22.0.0.2 contains several enhancements: We added support for OpenJFX in Liberica NIK for MacOS, so now you can develop rich JavaFX apps on your Mac; native-image is now included by default in all NIK flavors. There is no need to run gu to install it anymore. Both released versions of Liberica NIK contain the latest version of Liberica JDK with a number of fixes and solved security issues. Growing momentum of Liberica NIK with Spring Native In September 2021, we announced a support agreement with our partner, VMWare. It meant that VMWare customers using Liberica NIK could run Spring apps as native executables and receive full-fledged support. In addition, NIK is fully compatible with Spring Native — a recent addition to the Spring framework ecosystem that allows the compilation of Spring Boot applications into native executables. But Native Image Kit keeps fortifying its positions in the Spring world. As of Spring Native 0.11, it is used by default for Buildpacks native support. Conclusion BellSoft constantly works on improving the security and performance of its products. Future enhancements of Liberica Native Image Kit will include added AWT/Swing support for Windows and macOS and improved GC functionality. As for now, download the latest builds of Liberica NIK and enjoy all the benefits of native image technology! Download Liberica NIK - [What is OpenJDK and why you should use it](https://bell-sw.com/announcements/2022/02/10/what-is-openjdk-and-why-you-should-use-it/): OpenJDK is the development kit for Java, the language with 26 years of history behind it. It is not a new language by any standards, especially in the industry where new solutions appear every few years and are usually better than their predecessors. And OpenJDK, the most popular instance of Java, is 14 years old already. By all means, OpenJDK should have been replaced many times already. And yet it is still one of the most popular development tools in the world! How could this happen? In this article we will try to answer this question, dive deep into the process of constant upgrading of OpenJDK, talk about open source approach to development, and describe all the things that make Java so unique. Table of Contents What makes Java and OpenJDK special? How OpenJDK came to be How OpenJDK works and what are the advantages How OpenJDK is being developed OpenJDK as the base for a race into space The process of upgrading the OpenJDK The release cadence Why open source is better than closed source Why are there so many OpenJDK vendors and implementations? Conclusion What makes Java and OpenJDK special? How OpenJDK came to be The history of Java began in 1991, when engineers at Sun Microsystems became frustrated with the C and C++ programming languages’ restrictions and APIs. The prototype of the language was initially named “Oak”, later renamed to Java. It first became available for download in 1994, with first public release in 1995, and almost immediately received support in the then popular Netscape web browser. In 2006 Sun released the Java HotSpot virtual machine and compiler as free software under the GNU General Public License, and since then the history of OpenJDK started with Java 6 and 7 being open source. During the period from 2006 and 2008, more and more code was being opened. Right now, OpenJDK’s code is available to anyone, and is developed by a large community situated around Oracle. There are many distributions of OpenJDK available, including Liberica JDK built by BellSoft, which is also freely available for anyone to explore and use in accordance with the open source approach. Discover Liberica JDK How OpenJDK works and what are the advantages There were several goals formed at the inception of Java, including: Java is simple, object-oriented, and familiar. Java is robust and secure. Java language is architecture-neutral, and JVM is portable. Java is able to execute with high performance. And these principles are still what makes OpenJDK popular, so it is important to explore each of them and see how they affect the life of developers and end-users. Java language is architecture-neutral and JVM is portable Java code is architecture-neutral, meaning that if written right, it can be launched on any device, from PC to modern microwave. This is made possible by utilizing the most important feature of Java 一 JVM, Java Virtual Machine, runtime used for executing applications. Usually most of the programming languages compile their code into machine code, that is different for any platform. Java compiler works differently, it creates the special “java bytecode”, that can be executed by Java Virtual Machine running on any system. The JVM in turn uses a dynamic compiler that compiles code during the execution of the app. This approach allows running the same code on completely different platforms with a little to none rewriting! That’s right, you can write the program once, then execute it anywhere, if you follow the guidelines. So if you ever wanted to play Tetris (or rule the world) using your modern microwave’s touch screen 一 Java is the easiest language to do it! But in all seriousness, this approach, called “Write Once, Run Anywhere”, allows you to not have to deal with different architectures, OSes, drivers, or hardware. The only thing you need to do is to utilize the virtual machine for the chosen platform, and there are a lot of them supported, more in the works. Java is simple, object-oriented, and familiar These days, Java syntax is not the easiest anymore… at least for now, as there are many new features in the works that make the code easier to read and write. And yet, at the time of its conception it was much more organized than its competitors. The Java Language Specification is the set of rules that ensures that all the innovations follow the same logic and are created in accordance with Java ideology. All the APIs are created to be compatible with each other and Java specification, so it has everything you need to not only write your code effectively, but inspect and utilize other people’s work. New ways to program that implement new technologies, like pattern matching for switch and Vector API, are being constantly implemented. Java is robust and secure Being an open source project, OpenJDK’s code is available for anyone who wants to check or enhance its security. It means that newfound vulnerabilities are constantly being patched with quarterly CPU releases and in the case of Liberica JDK fixed by our support. And with the wide selection of security tools like vulnerabilities scanners you can be sure that the attempts to breach the security of your apps and infrastructure will not succeed. Java is high-performant There are many ways to boost your OpenJDK apps. The compilers inside the JVM optimize the code for peak performance. If the startup time is more important, you can use GraalVM native image technology that packs the applications together with the runtime into a single executable binary file. To find out why Java works best for your development contact our engineers. Contact our experts How OpenJDK is being developed OpenJDK as the base for a race into space You can compare a programming language to the spaceport the spaceships are launched from. When developers build their applications, “the spaceships”, they expect the infrastructure to work and make their life easier. It means that every mistake in the functioning of the programming platform and runtime will put them into a dangerous situation, when they did everything right, but the launch failed, causing the waste of time and money. So why do they choose OpenJDK? Because it is the “spaceport” managed by a reliable and large community. Let’s find out how everything works in OpenJDK. The process of upgrading the OpenJDK The development of OpenJDK could be divided into several directions: Bug fixing; Implementation of new features and the deprecation of the outdated features; The projects outside of the main branch. Let’s discuss them all in-depth. Bug fixing Today, when GitHub is utilized for OpenJDK development, the process is mostly automated and considerably faster than it used to be. To report a newfound bug, a developer uses the mail list to describe the error, which is then given a number.. When a developer wishes to fix a bug, they create a fork in the repository, which is basically a copy of the whole project. Then in this fork, the developer edits the necessary part of the code to fix the bug. After the successful testing, they make a pull request with the new code back into the main branch. The code is then reviewed and, in case it is satisfactory, implemented into the main branch for the next release of OpenJDK. That system allows any developer to try and fix any found bug. Implementation of new features and deprecation of the outdated features New features are usually implemented in a few steps. First, it is proposed as a concept (draft) by developers. The member of the Governing Board called OpenJDK lead decides if the feature is worth working on. After the approval, the draft is updated with the necessary description and the proposition of basic implementation, becoming the JEP (JDK Enhancement Proposal). It goes through many stages before completion. The implementation process itself is similar to the bug fixing described earlier and is done via fork branches on GitHub. This process could take a long time to finish as the new functionality is released in stages. The Previews deal with semantics and syntax, the Incubators 一 with API changes. There could be as many as 3 previews or incubators before the final implementation of the feature is released. This is done to allow the developers to test the new functionality and see how well it works with the new code and the existing one, so as not to break the already working applications. Usually after 2 previews or incubators, the new code is either added to the release in final form or removed if deemed unsafe or underperforming. The projects outside of the main branch. Some projects that affect the functionality of Java greatly are developed outside of the main branch. If successful they drastically change the language, but they take a lot of time to be polished. They can be still implemented into OpenJDK in their current state if needed, and some developers routinely do this. In time they become JEPs and a part of the next release. Changes to the specification Specification upgrades are proposed and developed in the form of JSR 一 Java Specification Request. These go through the public review and vote by the Executive Committee consisting of companies and people guiding the evolution of Java technology. BellSoft is also a part of that committee along with companies such as Alibaba, Amazon, IBM, Intel, and Oracle. The release cadence There are three kinds or releases of OpenJDK: Quarterly CPU release discussed earlier. It is built with security in mind and consists of vulnerability and bug fixes. Technical release 一 a numbered release with new features, intended mostly for trying them out. It is useful for developers to test the new technologies, get their apps ready for deprecation of the old ones, and plan their future work. LTS (Long Term Support) numbered releases. These are the ones full of tested implementations and will be supported by vendors for the longest time. They are meant to be the base to develop and run the programs in an enterprise environment. Before Java 9 release in 2017, it could take up to almost 5 years for a new numbered release to come out. Since then, Oracle switched to the new release cadence with two numbered releases every year and a new LTS release every two years. With Java 17 being the latest LTS release, the next one will be Java 21 in September 2023, and the following ones will be released every two years. The support itself is provided by many vendors, including BellSoft, with new versions of Liberica JDK released at the same time as other major distributions. Why open source is better than closed source It is important to remember that OpenJDK is an open source project. It matters, because all the closed source alternatives are essentially the black boxes. Could they be safer in theory? Yes. But also in theory, they could contain a large number of vulnerabilities no one knows of… or maybe no one but the data thieves who already use the bugs to steal money or information with developers none the wiser. In open source, this is impossible. You always know what works, what does not, and why. Sure, the malicious actors can study the code, but a large number of developers and security managers also can! All the vulnerabilities are quickly discovered and fixed. All the ciphering algorithms and security measures are available for testing. And one more thing 一 if you think you can do something better than it’s done 一 you can just go ahead and do it! Which leads us to the next question. Why are there so many OpenJDK vendors and implementations? Outside of a raw OpenJDK, there are a lot of vendors who sell or provide for free their own builds, including BellSoft that makes Liberica JDK. Why are there so many to choose from and what are the differences? The answer is simple 一 functionality, security and support. All the Java Development Kits, including Oracle Java SE, are based on OpenJDK. They all can run the Java code just the way it is intended to run, and they are similar when everything is working fine. The differences arise when something is wrong. If you use the OpenJDK in enterprise and you face the problem, you need experts to deal with it. You either have to keep your own team dedicated to maintaining the stability of OpenJDK (which is costly and not always necessary) or contact the people who can help. In the case of OpenJDK, you need to go through all the channels described earlier to fix the bug or to get the answer and wait in line. Now, the vendors are there to help their clients. Some of them do that efficiently and fast, some demand you first speak with the managers and lawyers, taking your time and making you lose money. We in BellSoft strive to provide you with high-powered support with a response time as fast as 1 hour and the extended support timeline for older Java versions, including 6 & 7. Other advantages of OpenJDK builds like Liberica JDK are the additional security enhancements and functionality that make your applications even safer. Click the button below to get a comprehensive summary of Liberica JDK features. Get white paper on Liberica JDK Conclusion OpenJDK is an amazing platform for app development and it only gets better with age! We invite you to join the large developers community that utilize Java daily. Each of us works to make it better, and this is the best guarantee that Java will live on many years from now. - [Oracle Java License Change: Complete Guide (2026)](https://bell-sw.com/announcements/2022/02/24/java-licensing-changes-in-2021/): Oracle has made three licensing changes since 2019 for its commercial Java Platform, Standard Edition (Java SE). The cumulative effect is that many organizations that were previously using Java without a subscription now require one — and the cost of that subscription has changed significantly. Since Oracle began actively encouraging customers to sign up for subscriptions, many organizations have opted to replace Oracle Java with one of several available free OpenJDK distributions. This article explains what has changed, how to determine whether your organization is affected, and what the available options are. Table of contents Find Your Situation What Changed — Three Licensing Updates Since 2019 First change: April 2019 — subscription required for security updates Second change: September 2021 — NFTC license introduced for new LTS releases Third change: January 2023 — employee-based subscription metric What the 2023 change means for total cost of ownership How Do These Changes Affect Your Java Environment? 3 Steps to Manage Your Exposure Step 1: Inventory — map your Oracle JDK deployments Step 2: Assess — identify what requires a license and calculate the cost Step 3: Decide — evaluate options by cost, risk, and engineering effort Liberica JDK Frequently Asked Questions Find Your Situation The table below provides a quick reference by Java version. Full details on each scenario follow in the sections below. You are on… Situation Recommended action JDK 6, JDK 7 (≤7u80) or JDK 8 (≤8u202) Technically license-compliant under BCL, but no security patches for years. Running in production is a significant security risk. Switch to an OpenJDK distribution that still provides security patches for these versions. No code changes required. JDK 8 (≥8u211) or JDK 11 (any) ⚠️ Requires a commercial Oracle Java SE subscription for production use. OTN license permits development and testing only. Purchase a subscription, or switch to an OpenJDK distribution (see Option 4 below). JDK 17 (≤17.0.12) Technically NFTC-compliant. Oracle no longer provides free security patches. Not recommended for production. Upgrade to JDK 25 or switch to an OpenJDK distribution for continued free security patches. JDK 17 (≥17.0.13) ⚠️ Requires a commercial Oracle Java SE subscription. The NFTC license expired for these releases in September 2024. Purchase a subscription, upgrade to JDK 25 (Option 2), or switch to an OpenJDK distribution (Option 4). JDK 21 ✅ Free under NFTC until September 2026. After that, security patches require a subscription. Upgrade to JDK 25 or switch to an OpenJDK distribution before the NFTC window closes. JDK 25 ✅ Free under NFTC until September 2028, one year after Java 29 LTS (planned September 2027). Currently the safest Oracle JDK option for production use. Evaluate an OpenJDK distribution to avoid the next license deadline. Non-LTS (e.g. JDK 26) Available under NFTC for the entire planned six-month support life. No further updates after that window closes. Upgrade to the next release before the six-month window closes. Verify current terms at oracle.com/java/technologies/javase/jdk-faqs.html. Oracle may revise license terms at any time. What Changed — Three Licensing Updates Since 2019 First change: April 2019 — subscription required for security updates The first licensing change became effective in April 2019. Oracle replaced the Oracle Binary Code License (BCL) with the Oracle Technology Network (OTN) License Agreement. Under the OTN license, Oracle requires users to purchase an annual subscription to obtain critical security patches for Oracle JDK used in production workloads. This change applied to the Java Runtime for Java 7, 8 and 11. The last free Oracle JDK 8 update was 8u202, released in January 2019. All subsequent Oracle JDK 8 updates — from 8u211 onwards — require a subscription. All releases of Oracle JDK 11 require a subscription. The OTN license does permit free use for development, testing and demonstration, but not for production workloads. As of October 2024, Oracle had released 213 security patches since the last free update (Source: Gartner, January 2025). Nearly all these vulnerabilities can be exploited over a network without authentication. Organizations running unpatched production workloads carry significant security risk even if no license violation is present. Second change: September 2021 — NFTC license introduced for new LTS releases Starting with Java 17 in September 2021, Oracle began licensing new releases under the No-Fee Terms and Conditions (NFTC) license. This permits free commercial use, including production workloads, for a defined period. For LTS releases, Oracle provides free quarterly updates until one year after the next LTS release. Oracle released JDK 25 in September 2025; accordingly, free updates for JDK 21 are available until September 2026. Free updates for JDK 25 are planned until September 2028, one year after Java 29 LTS, which Oracle currently plans to release in September 2027. See the Oracle Java SE Support Roadmap for the current schedule. The NFTC license expired for Oracle JDK 17 in September 2024. Build 17.0.12, released in July 2024, was the last free update. All subsequent JDK 17 releases are licensed under the OTN agreement and require a subscription for production use. Non-LTS releases — such as JDK 24 and JDK 26 — are available under the NFTC for their entire planned six-month support life. No further updates are provided after that window closes. Important: The NFTC license does not apply to previous LTS releases. It does not apply to Java 8, 11 or 17 (post-17.0.12). It also does not apply if an organization has previously received that JDK version under a paid Oracle Master Agreement or Java SE subscription. Third change: January 2023 — employee-based subscription metric The third licensing change became effective in January 2023. Oracle revised the default Oracle Java SE subscription model, replacing the previous Named User Plus (NUP) and Processor metrics with a single Employee metric. Under the Employee metric, an organization must license all employees regardless of whether they use Oracle Java. Oracle defines "employees" to include all full-time, part-time and temporary staff, as well as all agents, contractors, outsourcers and consultants that support the organization's internal business operations. "...all of Your full-time, part-time, temporary employees, and all of the full-time employees, part-time employees and temporary employees of Your agents, contractors, outsourcers, and consultants that support Your internal business operations." — Oracle Java SE Universal Subscription license terms This metric change often generates a significantly higher annual fee. According to Gartner, most clients indicate the new subscription model is two to five times more expensive than the legacy model. House of Brick Technologies calculated that a medium-sized company could experience a cost increase of up to 1,400%. What the 2023 change means for total cost of ownership The shift to employee-based pricing decouples cost from actual Java usage. An organization with 10,000 employees but only 50 servers running Java faces the same license metric as one with Java on every desktop. The table below illustrates how this affects organizations of different sizes, compared to a server-based OpenJDK distribution support model. Employee Band Monthly Rate / Employee Annual Cost / Employee 1 – 999 $15.00 $180.00 1,000 – 2,999 $12.00 $144.00 3,000 – 9,999 $10.00 $120.00 10,000 – 19,999 $8.00 $96.00 20,000 – 29,999 $7.00 $84.00 30,000 – 39,999 $6.00 $72.00 40,000 – 49,999 $5.25 $63.00 50,000+ Contact Oracle Custom quote Source: Oracle Java SE Subscription FAQ. Prices subject to change. Verify current rates at oracle.com. Existing subscribers: Although Oracle no longer offers the legacy NUP and Processor metrics to new customers, existing customers may be able to renew under legacy terms. Oracle does not publish a price list for the legacy model; prices may vary. When preparing for a negotiation, organizations should calculate the subscription cost under both the legacy and current metrics. How Do These Changes Affect Your Java Environment? The key practical question for most organizations is: which version of Oracle JDK are we running, and what does that mean for our compliance and security position? The table below maps every current Oracle JDK version to its license status, patch availability, and recommended action. Version Status License Free patches? JDK 6 & 7 (≤7u80) Compliant under BCL, but no security patches for years. End of life. BCL (historical). Free to run. No JDK 8 (≤8u202) Compliant under BCL. 213+ security patches released since January 2019. BCL (historical). Free to run; updates require subscription. No JDK 8 (≥8u211) ⚠️ Subscription required for production use. OTN — dev/test free. Production = paid subscription. Subscription only JDK 11 ⚠️ All releases require a subscription for production use. OTN — dev/test free. Production = paid subscription. Subscription only JDK 17 (≤17.0.12) NFTC-compliant. Oracle stopped providing free security patches in September 2024. Not recommended for production. NFTC (no further free updates from Oracle). No (ended Sep 2024) JDK 17 (≥17.0.13) ⚠️ NFTC expired. Subscription required for production use. OTN — subscription required for production. Subscription only JDK 21 ✅ Free under NFTC until September 2026. Plan for next transition. NFTC for all users. OTN applies after September 2026. Yes (until Sep 2026) JDK 25 ✅ Free under NFTC until September 2028. Current recommended Oracle LTS. NFTC for all users. OTN applies after September 2028. Yes (until Sep 2028) JDK 26 (non-LTS) Free under NFTC. Six-month support life. NFTC for all users. Yes (until Sep 2026) JDK 27 (non-LTS) Free under NFTC. Six-month support life. NFTC for all users. Yes (until Mar 2027) 3 Steps to Manage Your Exposure The following three-step framework helps organizations understand the scope of their Oracle Java SE subscription requirement, calculate costs across all available models, and make an informed decision about the most appropriate path forward. Step 1: Inventory — map your Oracle JDK deployments Before you can assess exposure or calculate cost, you need to know what you have. Work with your software asset management (SAM) function and operations leaders to create a comprehensive inventory. This inventory should document: Which versions of Oracle JDK are deployed and where. Which applications are using Oracle JDK and whether those are production, development or test deployments. Whether server virtualization is in use. Oracle typically requires licensing every physical core in a virtualization estate, regardless of how many virtual cores are running Oracle JDK. Whether any third-party applications bundle Oracle JDK. Check with those vendors about their Java licensing position. Treat this inventory as an ongoing process rather than a single point-in-time exercise. Regular maintenance is required to ensure continued compliance. Step 2: Assess — identify what requires a license and calculate the cost With your inventory in hand, you can now determine which of your deployments require a commercial subscription and calculate what each available path will cost Which scenarios require a subscription? If anyone in your organization has downloaded any Oracle Java SE updates since April 2019, you probably need a subscription, and you may have a compliance risk. The practical scenarios that require an Oracle Java SE subscription are: Any Oracle JDK 8 build from 8u211 onwards deployed in production. Any Oracle JDK 11 build deployed in production. Any Oracle JDK 17 build from 17.0.13 onwards deployed in production. Any Oracle JDK version where developers use production tooling as part of their work — development use typically also requires a subscription. Scenarios that do not require a subscription include: running Java workloads exclusively on Oracle Cloud Infrastructure (OCI) or Oracle Private Cloud Appliance (PCA); using Oracle JDK exclusively for development, testing and demonstration; running Java applications that include a bundled redistribution license; using Oracle JDK 8 versions released before April 2019 (8u202 or earlier); using Oracle JDK 17 versions released before October 2024 (17.0.12 or earlier); and using Oracle JDK 21 or Oracle JDK 25 under the current NFTC license. Note on bundled licenses: Oracle includes a restricted-use license to use Oracle JDK with all licensed Oracle Fusion Middleware and stand-alone Oracle middleware products. This bundled license applies exclusively to Oracle applications — it does not extend to other Java workloads in the same environment. Calculate your Oracle subscription cost — current Employee metric The Oracle Java SE Universal Subscription is priced per employee — every employee in your organization, not just those who use Java. The pricing tiers below apply to the current model. Employee Band Monthly Rate / Employee Annual Cost / Employee 1 – 999 $15.00 $180.00 1,000 – 2,999 $12.00 $144.00 3,000 – 9,999 $10.00 $120.00 10,000 – 19,999 $8.00 $96.00 20,000 – 29,999 $7.00 $84.00 30,000 – 39,999 $6.00 $72.00 40,000 – 49,999 $5.25 $63.00 50,000+ Contact Oracle Custom quote Source: Oracle Java SE Subscription FAQ. Prices subject to change. Verify current rates at oracle.com. Compare: Oracle subscription vs. OpenJDK distribution support The table below compares the annual cost of Oracle's employee-based subscription, the legacy Oracle Processor model, and Liberica JDK Enterprise support, which is priced per server, not per employee. For organizations with large workforces and relatively modest server counts, the difference is substantial. Servers Employees Oracle Java SE subscription / year Liberica JDK / year* 50 250 $45,000 ~$15,000 500 2,500 $360,000 ~$70,000 1,000 10,000 $990,000 ~$90,000 2,000 50,000 $3,150,000 ~$165,000 *Liberica JDK Enterprise support is priced per server, not per employee. See bell-sw.com/support/ for published pricing. Use the interactive savings calculator to estimate your specific cost reduction. Oracle Vigorously Pursues License Compliance According to Gartner client interactions, Oracle actively targets organizations on Java compliance, including both existing Oracle customers and organizations without any other Oracle products, and deploys its global Java licensing team to enforce compliance. Most of this activity takes the form of compliance outreach rather than formal audits. Oracle may notify organizations of licensing changes or potential compliance issues and request time to discuss the matter. Oracle may also assert compliance issues based on publicly available employee counts or request that organizations provide Java deployment information. If Oracle contacts your organization about Java licensing, it is advisable to validate your information-sharing obligations under any applicable Oracle agreement before responding. Treat informal compliance outreach with the same care you would apply to a formal audit notice. Organizations that identify and resolve compliance gaps before any Oracle outreach are in a significantly stronger negotiating position than those responding to a contact they did not anticipate. A regular Java inventory, conducted at least annually, and updated whenever new software is deployed or decommissioned, is a sound practice. A single unmanaged Oracle JDK installation in a production environment is sufficient to bring the entire organization within the scope of the Universal Subscription. The steps outlined below provide a framework for calculating that exposure and evaluating the available responses. Step 3: Decide — evaluate options by cost, risk, and engineering effort With a clear picture of your deployments and the cost of each path, you can now make an informed decision. The right answer depends on three factors: the financial cost of each option, the security and compliance risk of doing nothing, and the technical effort required to implement Option 1: Revert to the last free update This option has no subscription fees but carries significant security risk. It should be considered only as a short-term measure while a longer-term solution is implemented. This option involves removing all deployments of Oracle JDK beyond the last free build (8u202 for JDK 8; 17.0.12 for JDK 17) and returning to those versions. The effort required is relatively modest. The security risk is substantial. Beyond the immediate vulnerability exposure, organizations running unpatched releases may find that customers or end users decline to accept their products on these versions, that security auditors will not allow workloads on these releases to pass compliance checks, or that cyber insurance policies become difficult to maintain. When those pressures arrive, remediation typically happens under time constraints rather than on a planned schedule. This option is not recommended as a permanent solution. Organizations adopting it should have a clear plan and timeline for implementing one of the alternatives below. Option 2: Upgrade to Oracle JDK 25 This option eliminates the subscription requirement on a durable basis but requires a significant amount of software engineering work and may not be achievable for all applications in the near term. Upgrading all Java applications to JDK 25 would eliminate the requirement to purchase an Oracle Java SE subscription. Oracle JDK 25 is available free of charge under the NFTC license until September 2028. The amount of engineering effort depends on the number of applications, which versions those applications currently use and whether they are homegrown or purchased. An upgrade from JDK 8 to JDK 25 requires code modifications and comprehensive testing. Purchased applications will typically require upgrading to current vendor-supported versions, and not all application vendors may yet support JDK 25. Planning note: Organizations that are not able to complete the upgrade before September 2028, at which point the JDK 25 NFTC window closes, may need to purchase a subscription during the transition period. A subsequent upgrade to JDK 29 will be required before that date to maintain continued free access to updates. A continuous upgrade practice is required to remain on the free LTS release at all times. Upgrading to Oracle JDK 25 is a viable long-term option for organizations prepared to invest in the engineering work required and to adopt a regular JDK upgrade cadence. Option 3: Switch to an OpenJDK distribution This option is quite viable and, for most organizations, requires a relatively modest amount of testing with minimal reprogramming. Purchasing support from an OpenJDK vendor provides a guarantee that the behavior of your workloads will not change during migration, giving your team confidence and significantly reducing the internal overhead of the transition. OpenJDK distributions, such as BellSoft Liberica JDK, are based on the same open-source code as Oracle JDK. For the vast majority of applications, they are drop-in replacements that require no changes to application source code. Switching requires removing Oracle JDK from all desktops and servers and replacing it with the chosen distribution. Most OpenJDK vendors support JDK 8, 11, 17, 21 and 25. This means organizations are not required to change their Java version as part of the migration. An application currently running on JDK 8 or JDK 11 can switch to an OpenJDK distribution of the same version and continue to receive free security patches. BellSoft provides security patches for JDK 8 until 2031. For environments with custom builds or complex configurations, purchasing commercial support from an OpenJDK vendor is advisable. This provides certified expertise in JDK internals, a defined path through the migration, and accountability for the outcome, reducing the risk and internal effort compared to managing the migration independently. Two considerations may limit the applicability of this option. First, some application vendors specify that their products must run on Oracle JDK; those support agreements may require renegotiation. Second, OpenJDK distributions do not include the proprietary Java Plug-In for browser applets, though open-source alternatives are available. Organizations with applet-based applications should prioritize their modernization. Liberica JDK BellSoft Liberica JDK is a TCK-verified OpenJDK distribution available free of charge for commercial use under an open-source license. Commercial support subscriptions are available and are priced per server rather than per employee. Liberica JDK supports JDK 6, 7, 8, 11, 17, 21 and 25 across a wide range of operating systems and hardware architectures. Enterprise subscribers receive access to custom builds, extended LTS support, Liberica JDK Performance Edition, Native Image Kit, and pre-built container images with Liberica JDK Lite, along with direct access to Java engineers to guide your migration and ensure a successful outcome. For a full comparison of available OpenJDK distributions, see the OpenJDK distributions comparison guide. Download Liberica JDK — free | Calculate your savings | View enterprise support pricing Frequently Asked Questions Is a subscription required if I am on JDK 8u202 or an earlier release? No. Build 8u202 and earlier releases of Oracle JDK 8 were licensed under the BCL, which permitted free commercial use. These builds remain technically license-compliant. However, as of October 2024, Oracle had released 213 security patches since that release. Running them in production represents a significant security risk. The OTN license permits free use of Oracle JDK for development, testing, prototyping and demonstration. However, developers who use production tooling as part of their work are generally considered to require a subscription. Consult your Oracle agreement for the specific terms applicable to your situation. Can existing subscribers renew under the legacy NUP or Processor metric? Oracle no longer offers the legacy metrics to new customers, but existing subscribers may be able to renew under legacy terms. Oracle does not publish a price list for the legacy model. When preparing for a negotiation, calculate costs under both the legacy and current Employee metrics to inform your position. Is Java 21 free for commercial use? Yes, under the NFTC license until September 2026, one year after Oracle JDK 25 LTS was released in September 2025. After that date, Oracle intends to apply the OTN license for subsequent JDK 21 updates, the same license currently used for Java 8, 11 and 17. Is Java 25 free for commercial use? Yes, under the NFTC license until September 2028, one year after Java 29 LTS, which Oracle currently plans to release in September 2027. After that date, Oracle intends to apply the OTN license for subsequent JDK 25 updates. Can I avoid a subscription by switching to an OpenJDK distribution? Yes, provided that all Oracle JDK deployments are removed from your environment and replaced with an OpenJDK distribution. If any single application continues to use unlicensed Oracle JDK, a subscription is required. Individual applications can use different JDK distributions. It is not necessary to standardize the entire estate on a single product. What is the difference between Oracle JDK and an OpenJDK distribution? Oracle JDK and OpenJDK distributions are built from the same open-source codebase and pass the same Technology Compatibility Kit (TCK) compatibility tests. Oracle JDK requires a commercial subscription for sustained production updates. OpenJDK distributions from vendors such as BellSoft, Amazon, Eclipse Adoptium and Azul are available free of charge under open-source licenses, with optional commercial support subscriptions. References and Useful Links Oracle JDK License General FAQs — oracle.com Oracle No-Fee Terms and Conditions (NFTC) License — oracle.com Oracle Technology Network (OTN) License Agreement for Java SE — oracle.com Oracle Java SE Support Roadmap — oracle.com Oracle Java SE Subscription FAQ — oracle.com BellSoft: Comparison of OpenJDK distributions — bell-sw.com Liberica JDK downloads — bell-sw.com Liberica JDK Enterprise support pricing — bell-sw.com Savings calculator: Oracle Java vs Liberica JDK — bell-sw.com Disclaimer: BellSoft is a Java runtime vendor. Nothing in this article constitutes legal or compliance advice. Consult a qualified Oracle licensing specialist or legal counsel for advice specific to your situation. - [How to lower TCO by choosing the right OpenJDK vendor](https://bell-sw.com/announcements/2022/03/17/how-to-lower-tco-by-choosing-the-right-openjdk-vendor/): Contents The art of cutting the expenses The overall costs of migration The whole is greater than the sum of the parts Easy migration without additional cost Expanded high quality Long Term Support Small and fast Unification Arm expertise Enhanced security How we reduce the cost Customers who have reduced their TCO Let us decrease your TCO The art of cutting the expenses Decreasing cost is a hot topic in the OpenJDK world. We at BellSoft would like to explain how we do it, what steps we take to help you avoid unnecessary spending, and provide a way to estimate how much you will save! In this article, we will look at the actual costs of Java and show you Belllsoft’s approach to saving you money. WIth Bellsoft’s Liberica JDK we can reduce a customer’s TCO by 90%. Cost reductions come from: Paying less for Java support Avoid wasting money by minimizing the risks of dealing with vulnerabilities Enjoy Liberica JDK features that benefit your workflow, security, and energy efficiency Read on to understand better how we make your costs go down and simultaneously enhance the efficiency of your apps. We will explore in-depth every money saving component of utilizing our runtime, so you will understand where the big number of 90% TCO reduction comes from. Try Liberica JDK for free The overall costs of migration Let’s break down all the straightforward cost benefits of moving from Oracle’s JDK to Liberica JDK Lower support cost: The price of our support is one of the lowest on the market, and its quality is one of the best. We guarantee at least 30% cost reduction after migrating from Oracle just in support cost alone Flexible pricing: Flexible prices that we adjust to meet your request. You can choose to pay only for the things you actually require. We will not push the services you don’t need onto you. Depending on the size of your fleet, you can reduce the costs you pay for a single runtime you use One of the arguments of switching to Oracle Java is the new 2-year free support for the Java 17 users. However, Oracle requires you to always keep your runtime on the latest version, constant updates and you can risk losing the functionality of your apps. All the reasons Oracle Support is not actually “free” are listed in this article. The whole is greater than the sum of the parts The cost of support is certainly important for lowering your TCO, but there are additional factors to consider when evaluating the cost reduction. There are several ways we help with saving your money and protecting you from additional spendings. Easy migration without additional cost Liberica JDK is fully compatible with all your previously developed Java applications. Our support team helps you with the setup and running the runtime — making the migration fast, simple and safe. The migration is fast, simple and safe. Expanded high quality Long Term Support We support the more dated versions of Java longer than our competitors, allowing you to upgrade when you need to without any hurry. Note that you can utilize Java 6 and 7 with regular security updates and multi-platform support if you have absolutely no opportunity to upgrade (although we highly recommend switching to at least Java 8). Furthermore, the quality of our support means Your services and application will be stable New projects won’t stop being developed to find a fix for the runtime problem You do not need to keep a team dedicated to the runtime maintenance Our support team works 24/7/365, we provide hotfixes to our customers momentarily, and ensure the quality of our runtime. Small and fast Liberica JDK is perfect for building tiny containers, and Liberica Lite makes their size even smaller. That means you will benefit from Faster uploads to server Startup speed Memory footprint These enhancements result in services performing and deploying faster, taking less space in the cloud and putting a smaller burden on the server. And that leads to spending less on Cloud expenses. Another tool we provide to lower your costs is the Liberica Native Image Kit, which creates small native images. They have all the advantages of small containers we mentioned earlier, including a smaller burden on the server, but also all the bonuses of using the GraalVM. Try Liberica NIK for free Unification You can use Liberica JDK as the unified Java runtime for all of your clouds and computers. That means Paying for a single support plan Ensuring the compatibility (including the older Java versions) Easy managing, requiring less employees Furthermore, to make the control of a large Java runtime fleet even more convenient, we created the Liberica Administration Center. With this tool, just one person can manage, update check licenses and secure all machines and clouds running Liberica JDK! That saves you money on salary and runtime handling. Arm expertise BellSoft is the team that made AARCH64 port of OpenJDK performant by proposing and integrating JEP315 back in JDK11. Today BellSoft is one of the most active contributors in AARCH64 and ARM32 ports and our engineers maintain the ARM32 port of OpenJDK. In other words, our engineers are the world-renowned experts in ARM CPU support for Java runtime. That is why Liberica JDK builds, including the one created for embedded systems, works great on ARM CPUs. And their costs, including maintenance, is much lower than that of x86 and x64 CPUs, saving you even more money. Enhanced security Keeping your and your clients’ data safe is not only good for your reputation and business, but also is the only way to ensure you won’t become a defendant in a court in a case about stolen information. For example, one such known case ended in an $8m fine. Liberica JDK is built with enhanced security in mind. BellSoft releases quarterly Critical Patch Updates (CPU) in addition to the numbered versions. Furthermore, our security team builds extra patches for customers when needed, and all the issues found for one client are fixed in the next security release for everyone. How we reduce the cost To sum up, your cost reduction depends on the size of your fleet and services you require, plus a number of expenses you could possibly avoid. Below is an overview of the benefits of migrating to Liberica JDK and how we lower TCO of your Java runtime. Customers who have reduced their TCO Here are a few cases where our clients reduced their TCO by switching to Liberica JDK. Our first example is a modern finance organization, specializing in exchange-traded products and cryptocurrency. The nature of their work requires an impenetrable security to keep their clients’ data safe, so the cost of protecting the runtime was always high. We offered them a fail-safe runtime with extra security updates and enhancements with competitive support price. That made their TCO go down 92% 一 even lower than we promised to deliver! Another example is a global appliance stores network. The pandemic and lockdown pushed them to focus on implementing the new online features, but had to maintain a stable communication with stores and warehouses to calculate the quantity of wares in need of disposal and transfer. Our cloud solutions allowed the client to lower the costs by 15% and make the cloud environment much more stable. That in turn minimized their risks of down time and losing money. And the last example is our partnership with VMware, which resulted in Liberica JDK becoming a default runtime for Spring, and Liberica Native Image Kit 一 for Spring Native. This is another proof of security, stability and performance of our products. If you want to estimate how much money we can save you in your particular case, contact our engineers for a free consultation. Get free advice Let us decrease your TCO Moving to Liberica JDK not only gives you many tools and opportunities, but also reduces the cost of support and total overall cost, including the risk of losses, and provides additional tools. And don’t forget, we are ready to adjust the prices so you only pay for the services you need. Get in touch with our sales managers so we can discuss all the options. Contact us! - [Liberica JDK 18 release: the future of Java development](https://bell-sw.com/announcements/2022/03/23/liberica-jdk-18-release-the-future-of-java-development/): Contents Java language becomes better What is next in line for Java Implementing projects outside of main branch Enhanced cloud development technologies New platforms and improved support for existing ones Easy interoperability with non-Java code To migrate or not — the new old question Back to Java 18 Conclusion — the future is here OpenJDK 18 is not a Long Term Support release, but that doesn’t mean it’s less interesting or not as important. Non-LTS releases are a great way to test features soon to be available in a future LTS release. They also provide a smoother transition to the latest versions. We already had an in-depth discussion of Java 18 features and released a video dedicated to the new JEPs, so be sure to take a look for a more comprehensive analysis. Instead, we’d like to focus on just what JEPs mean for the future of the Java platform. Java language becomes better One of the constant focuses of Java evolution is security because new vulnerabilities, like last year’s log4j issue, are discovered constantly in all programming languages. To make your apps safer and more stable, some enhancements were introduced. JEP 400: UTF-8 by Default will protect from the errors of localization or international usage of applications. JEP 421: Deprecate Finalization for Removal will get rid of the outdated finalization feature that could slow down your machine or cause memory leaks. Considering the convenience of usage, JEP 408: Simple Web Server provides an easy way to test your network apps, and JEP 413: Code Snippets in Java API Documentation makes commenting and reading the code much easier. But the most important trend we see is the new functionality, either introduced or polished in Java 18. JEP 416, JEP 417, JEP 418, JEP 419, and JEP 420 are all dedicated to bringing the modern APIs and features into Java, thus allowing it to maintain the top place in the world of contemporary software development. Want to know why Liberica JDK is great for your enterprise? Book a consultation What is next in line for Java We expect these trends to continue so we can make an educated guess at what the future holds for Java developers. So we thought we’d share some of our predictions here. In a year, we’ll see how well we did. Implementing projects outside of main branch There are many stand-alone projects in development, but the moment of their implementation into one of the next releases is getting closer. Project Valhalla (set to enhance the performance of applications and modify the way Java works with objects in arrays) is a good candidate. Project Loom that introduces virtual threads is not likely to be implemented before 2033, but the steps are made in that direction, including JEP 418 (Internet-Address Resolution SPI) that greatly benefits from it. Finally, Project Amber will also bring important improvements related to pattern matching, namely record patterns (JEP 405) for record values deconstruction and String templates (JEP draft) containing embedded expressions incorporated into the result of a string template expression. Enhanced cloud development technologies Microservices, containers, message brokers, and native images are not new, but are still actively developed and enhanced every year! In 2022, we can expect not only better stability, faster speed, and smaller size of these, but even the new operating system that will be fine-tuned for Java containers of any kind and will make them faster and even more secure. In fact, this one is not a guess — BellSoft will very soon release this new OS full of features and enhancements for Java development, and you can quote us on this. This will make the cloud utilization even more effective for running Java apps, and 2022 will be the year of the cloud deployment even for cases where people used to rely on their own dedicated servers. New platforms and improved support for existing ones In terms of architecture support and application compatibility, we consider Java still to be the very best — and the best will get even better in the coming year! The new RISC-V CPU family is a probable useful supplement to x86 and ARM processors in the near future. The concept of the architecture is based on free use and collaboration, making it a royalty-free production. As such it brings the open source values onto the hardware development. That is why many enterprises are very motivated to make it work, and the Linux Foundation announced the collaboration with RISC-V Foundation. For now it is practically utilized in embedded devices mostly, but we expect to see the new kinds of devices powered by this CPU soon, and possibly get the RISC-V support in the new Java LTS release. AArch64 and AArch32 are fully supported in Java, AArch64 support actively gets better with every new release. People who use Java 11 and up enjoy the enhanced performance with AArch64 CPUs, and the numbers keep getting more and more impressive. We expect to find new enhancements in the following versions of Java releases. And last but not least, new Mac models are getting improved support for AArch64 CPUs and a new graphic API. Easy interoperability with non-Java code Project Panama, which allows the smooth inclusion of foreign functions into Java apps, has seen a significant improvement. While we do not think it will be ready for the main branch in 2022, soon it might deliver the new breakthroughs that will solve the old problem of slower calls of native libraries. And if that happens, Java becomes the great language to build, for example, the neural networks and scientific applications that previously utilized Python or Julia. To migrate or not — the new old question A number of apps still work on Java 8 (and some even on Java 6 and 7), and it is easy to see why. It is still supported by many vendors (such as BellSoft), and there are many steps you need to take to migrate from 8 to 9 and higher versions due to radical changes introduced in Java 9. Even so, we think that 2022 might be the year many people will finally choose to move to the latest LTS version for several reasons. Last year there was the first LTS release in 3 years, and it is available long enough to be considered secure and reliable. The new release cadence will make the process of continuous migration to the newest Java version much smoother in the future. The end of support for Java 8 is coming closer. The modern libraries and development tools support Java 9 and up, some recent ones require even higher versions to function. Enterprises are already trying it out and giving feedback. It is important to mention that while the latest LTS Java version is 17, many developers will likely move to the previous LTS version, which is 11. It is considered to be just the right balance of stability and features. But those who utilize Spring in their development will choose Java 17. The new business challenges require modern solutions, and now you can actually test them. If you can afford an upgrade, we say — do that and join those who move the industry forward! Back to Java 18 Now that we’ve discussed the future of Java, let’s return to the present, which is the new release. As usual, it introduces the number of fixes and changes to the runtime. In summary: JDK — 2,131; FX — 124. As you see, the list of the fixed bugs in this release is very long. Check it out to see how much work the community has done to polish this release! Our engineers in BellSoft also participated by resolving 4 issues. Conclusion — the future is here Liberica JDK 18 is not just a new major release, but a clear example of how the world of Java development is getting more and more exciting. With the abundance of tools and some new projects to be announced soon, we can expect Java to gain even more popularity and receive new features. And with our TCO reduction advantages, Liberica JDK becomes the natural choice for your existing or future projects. Download Liberica JDK - [Alpine Linux/x64 is now officially backported to OpenJDK 11](https://bell-sw.com/announcements/2022/03/31/alpine-linux-x64-is-now-officially-backported-to-openjdk-11/): Following recent backports of Windows/AArch64 and macOS/AArch64 support of Java into OpenJDK 11, I proposed a similar integration of Alpine Linux/x64 (JEP 386). There are several reasons for that proposal: Alpine Linux is very effective for utilization in container environments due to its tiny size. A few OpenJDK vendors already provide patches that enable Alpine/MUSL support for 11u and 8u. There were no issues with those, so the proposed feature is already tested and working. The user demand for Alpine/MUSL support is strong. The BellSoft engineers are responsible for maintaining the Alpine port in OpenJDK 16, which makes them a natural choice to implement and support the backport. After the approval by the members of OpenJDK community, we finalized the process that involved working on the following JBS issues backports to 11u: JDK-8217340 Compilation failed: tools/launcher/Test7029048.java JDK-8247592 refactor test/jdk/tools/launcher/Test7029048.java JDK-8245938 Remove unused print_stack(void) method from XToolkit.c JDK-8252248 __SIGRTMAX is not declared in musl libc JDK-8252250 isnanf is obsolete JDK-8247589 Implementation of Alpine Linux/x64 Port JDK-8247591 Document Alpine Linux build steps in OpenJDK build guide JDK-8279958 Provide configure hints for Alpine/apk package managers We are pleased to inform you that the backport is ready and will be implemented into the main branch of OpenJDK 11. Feel free to test it and see how your containerized apps benefit from utilizing Alpine Linux. The change is expected to land in the 11.0.16 release in July 2022. - [How to use perf to monitor Java performance](https://bell-sw.com/announcements/2022/04/07/how-to-use-perf-to-monitor-java-performance/): I have already dedicated several posts to JDK Flight Recorder (JFR) 一 a built-in JDK profiler. While JFR is a great tool, it still has plenty of competition, as the ecosystem of profilers in the Java world is very abundant. Today I would like to focus on using one of such tools, perf 一 a powerful Linux profiler. Perf is a profiler built into the Linux kernel. It is full of various features, with a few of them pretty unique, such as: Profiling kernel execution Profiling based on hardware performance counters Perf can analyze the execution of kernel code alongside user space application code. This is especially convenient for troubleshooting IO performance issues. Another example of perf features is hardware performance counters — special CPU registers that track various kinds of low-level events (e.g. CPU cache misses). You can configure perf to use an internal CPU event for sampling, thus building a specific view of program performance, e.g., cache miss or branch misprediction hotspots. Below you will find a Linux perf tutorial for JVM applications and the comparison of perf and JFR. Using perf with JVM Let’s get down to practice and try profiling a Java program with perf. Perf requires Linux, an OpenJDK 17 distro (Liberica JDK in my case), and some Java code I’ve prepared. Below is a simple Java program that benchmarks the performance of the MD5 hash. import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random; import java.util.concurrent.TimeUnit; public class CryptoBench { private static final boolean trackTime = Boolean.getBoolean("trackTime"); public static void main(String[] args) { CryptoBench test = new CryptoBench(); while(true) { test.execute(); } } public void execute() { long N = 5 * 1000 * 1000; RandomStringUtils randomStringUtils = new RandomStringUtils(); long ts = 0,tf = 0; long timer1 = 0; long timer2 = 0; long bs = System.nanoTime(); for (long i = 0; i < N; i++) { ts = trackTime ? System.nanoTime() : 0; String text = randomStringUtils.generate(); tf = trackTime ? System.nanoTime() : 0; timer1 += tf - ts; ts = tf; crypt(text); tf = trackTime ? System.nanoTime() : 0; timer2 += tf - ts; ts = tf; } long bt = System.nanoTime() - bs; System.out.print(String.format("Hash rate: %.2f Mm/s", 0.01 * (N * TimeUnit.SECONDS.toNanos(1) / bt / 10000))); if (trackTime) { System.out.print(String.format(" | Generation: %.1f %%", 0.1 * (1000 * timer1 / (timer1 + timer2)))); System.out.print(String.format(" | Hasing: %.1f %%", 0.1 * (1000 * timer2 / (timer1 + timer2)))); } System.out.println(); } public String crypt(String str) { if (str == null || str.length() == 0) { throw new IllegalArgumentException("String to encrypt cannot be null or zero length"); } StringBuilder hexString = new StringBuilder(); try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(str.getBytes()); byte[] hash = md.digest(); for (byte aHash : hash) { if ((0xff & aHash) < 0x10) { hexString.append("0" + Integer.toHexString((0xFF & aHash))); } else { hexString.append(Integer.toHexString(0xFF & aHash)); } } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return hexString.toString(); } } class RandomStringUtils { public String generate() { int leftLimit = 97; // letter 'a' int rightLimit = 122; // letter 'z' int targetStringLength = 10; Random random = new Random(); StringBuilder buffer = new StringBuilder(targetStringLength); for (int i = 0; i < targetStringLength; i++) { int randomLimitedInt = leftLimit + (int) (random.nextFloat() * (rightLimit - leftLimit + 1)); buffer.append((char) randomLimitedInt); } return buffer.toString(); } } Compile the application, then run it with OpenJDK 17. java -XX:+PreserveFramePointer -cp . CryptoBench Additional option -XX:+PreserveFramePointer is required to allow perf to parse stack frames from Java code. Without this option, perf won’t be able to reconstruct the call tree of JIT-compiled methods. Results could still be useful if you are interested in native library or kernel hot spots triggered by your Java code. Let it run and use the second terminal to start profiling. The exact way to install perf depends on your Linux distro, as perf is a part of linux-tools package family. In some cases (like examples in this article), root is required to use perf, although it is possible to set up perf to be used by non-root users. Let’s find PID of our test program jcmd | grep CryptoBench Now we can run perf to collect stack trace samples (replace 1234 with PID you get after running the previous command). sudo perf record -F 500 -p 1234 -g -o perf1.data -- sleep 10 This command will sample all threads in the JVM process for 10 seconds using FP stackwalk to capture the traces 500 times per second. Data will be saved into the perf1.data file. We can try to run sudo perf report -i perf1.data. This command calculates a frame histogram and allows you to browse it conveniently. Perf collected stack trace samples in a data file. By inspecting the above screenshot, we can see that something is definitely happening in threads started by JVM, but most of the code is presented with obscure memory addresses. This happens because perf records only the addresses of the call chain from the stack. In the case of JVM, these would be either part of JVM binary or JIT-compiled code. To get the meaningful data, we have to utilize a symbol map to convert memory addresses into meaningful method names. For statically compiled code, symbols can be distributed alongside the binary. For JIT-compiled code, we have to create a code symbol map suitable for perf. It should be generated at runtime, preferably shortly after sampling is complete, as JIT-compiled code can be garbage collected over time, and addresses could be reused. By default, perf is looking for /tmp/perf-PID.map for a file with symbols for JIT-produced machine code. Several enhancements were introduced into JDK 16 concerning perf serviceability. For example, a new command line option, -XX:+DumpPerfMapAtExit, was added to write a perf map file on VM shutdown: perf record -- java -XX:+DumpPerfMapAtExit Workload Furthermore, starting with OpenJDK 17 we can generate a map file with jcmd like this: jcmd 1234 Compiler.perfmap Now, the same command sudo perf report -i perf1.data will give us a much more sensible report. The report now displays Java method names. Prior to OpenJDK 17, using perf with JVM was also an option, but it required an external tool (like perf-map-agent) to generate a symbol map. We added the -F 500 option to sample program execution with an approximate frequency of 500 Hz based on CPU cycles. Perf utilizes the CPU interrupts, so only threads running on the CPU are sampled. Default perf reporting mode is a frame histogram (similar to the hot method histogram in Java profilers). This way of presenting the data is convenient but usually requires some analysis to make sense. Built-in perf visualization options are pretty basic, although you can export raw samples with the perf script command and use 3rd party tools and scripts to generate more sleek-looking reports. Comparing JFR with perf using flame graph A flame graph is a visual representation style for stack trace sampling data. It’s a good starting point for analysis. By utilizing flame graphs, we can promptly compare data collected by perf with JFR-generated data, and a similar presentation style makes it easier. Creating flame graph from perf data We will use scripts from Brendan Greg to produce the flame graph with perf collected data. Let’s check out some scripts. git clone https://github.com/brendangregg/FlameGraph.git Now we can generate a flame graph for the data file we already have. perf script -i perf1.data | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > perf_flame.svg This command generates a report that looks like this: Creating flame graph with JFR If you aren’t familiar with JFR profiling, I suggest you read my previous article where I explore the topic. Below is a sequence of commands to capture data with JFR. > jcmd 1234 JFR.start settings=profile 1234 : Started recording 1. No limit specified, using maxsize=250MB as default. Use jcmd 1234 JFR.dump name=1 filename=FILEPATH to copy recording data to file. Wait for half a minute. > jcmd 1234 JFR.dump name=1 filename=perf1.jfr There are multiple ways to convert JFR data to flame graphs. My preferred method is using SJK. > wget https://bit.ly/2H3Uqck -O sjk.jar > java -jar sjk.jar ssa -f perf1.jfr --flame > jfr1.svg The above code generates a flame graph based on JFR sampling. Comparing flame graphs We can now effortlessly compare the results of JFR and perf. A closer look reveals that the graphs look sufficiently different. Perf includes samples from GC threads occasionally running Perf includes frames from JVM routines “CryptoBench.main” is replaced by “Interpreter” on the perf flame graph JFR shows diverse calls from “CryptoBench.crypt” including string manipulations missing on perf’s chart It is common for different profilers to display different results after inspecting the same application. The root cause of this discrepancy is a specific approach to sampling and data interpretation. Under the hood of sampling machinery Perf approach to sampling Perf sampling is driven by events, in the example above we used cycles (CPU cycles) as a driver for sampling. Once the event counter reaches the crossing sampling period, a stack trace is captured and recorded. Perf walking stack, assuming that frames are linked via FP (frame pointer), registers and collects return addresses. These addresses are further translated into symbolic method names at the time of reporting. Stack has to be immutable while it is being walked. Being a part of the Linux kernel, perf can execute its probe in the context of running a thread without freezing it (usually though interrupt). Yet, perf is still subject to ‘skid’, because even hardware interrupt cannot freeze thread execution in an instant. Provided both stack walk and thread interrupts are quick and efficient, perf can do sampling at very high frequencies (thousands of samples per core every second). When the program code is idle (e.g., waiting for semaphore or IO), it usually does not produce events and will be omitted from the sample population. This issue can be solved by the following feature — perf can trace thread state changes and IO-specific events separately using scheduler events. JFR approach to sampling Like most Java profilers, JFR uses internal JVM structures to reconstruct stack traces. There are multiple reasons why simple stack walking is not good for JVM: Java code can be interpreted or JIT-compiled, in the case of interpreted code, the address of interpreter runtime will be on the stack JIT compiler inlines method calls aggressively, so a single stack frame may represent multiple nested calls In the previous example, several calls have been included into the compiled body of the CryptoBench.crypt method. They become invisible to perf, but JFR shows them. JVM has information to reconstruct idiomatic stack traces in its internal data structure, although it cannot be accessed concurrently. A thread has to be stopped by the profiler (or the profiler should run its code on behalf of the thread). The easiest way to achieve that is the “Stop the World” pause 一 this is the way JVM thread dumps work, and many JVM profilers are using the same method for sampling. Stop the World pause is a relatively heavy operation, it also introduces bias into samples (infamous safepoint bias). JFR utilizes a more efficient approach. It stops each thread individually and does not rely on safepoints. It still requires a number of syscalls for each thread though. JFR method sampling also excludes idle (sleeping/blocking) threads from sampling. Safepoint bias While neither perf nor JFR relies on JVM safepoints, both are affected by some form of safepoint bias. By default, JVM emits symbols only for instructions, which can yield a safepoint, assuming that the stack trace can be calculated only under Stop the World condition. This behavior can be changed by adding the following options to the JVM start command -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints This affects both profilers, although JFR can still get a better picture thanks to virtual call sites decoding. See the flame graphs for the example we used previously, but with the -XX:+DebugNonSafepoints option added. Perf results JFR results As for JFR, the picture changes dramatically and provides more accurate symbol data, but this is not the case for perf results. Revealing JFR blind spots To further explore the topic of profiling inaccuracy, I would like to provide another example. The code is very simple. import java.util.Random; public class AllocatioNTest { private static int totalSize = 0; public static void main(String[] args) { Random random = new Random(); while (true) { extracted(random); } } private static void extracted(Random random) { int size = 8 * 1000 * 1000; int max = random.nextInt(10) + 1; int realsize = size * max; byte[] allocated = new byte[realsize]; totalSize += allocated.length; } } Below are flame graphs produced by profiling time code with perf and JFR. Perf results JFR results This example is one of the pathological cases for JFR. Perf shows us that a huge chunk of the main thread time is spent on memory allocation (which is an expected result for provided example code). The allocation happens in large chunks, so the _new_array_Java routine is called directly without inlining. JFR can decode only stack traces where Java code is executed, either compiled or interpreted. In this example, the CPU is caught in JVM routines or even the kernel most of the time. JFR discards such samples, thus creating gaps in the sample population and dramatically skewing the final distribution. Conclusion Perf is a powerful “pro” tool for the performance analysis on Linux. In this article, I have barely scratched the surface of perf capabilities. OpenJDK 17 removed one more hassle of using perf with Java by introducing a simple way to generate debug symbol maps of JIT-produced code under Linux. While it was previously possible to achieve the same results by utilizing additional tools, with OpenJDK 17, the overall process became more reliable and convenient. Perf is not the best tool for Java code profiling. Even with JIT symbol maps, method inlining and interpreted frames are too alien for perf to be handled accurately. And yet, perf is indispensable if you want to peek at the way your Java code interacts with native libraries or the kernel itself. It can also reveal the inner workings of JVM itself (e.g. GC or memory allocation), which most Java profilers cannot inspect. As a general principle, I recommend using perf as a complementary tool to JFR or one of the other Java profilers. In summary, A Java profiler can give you an accurate Java call tree Perf is more accurate for calculating a real on-stack percentage of frame, it has much higher sampling frequencies, and as such — higher accuracy Perf can give additional insights on how the CPU is utilized in JVM, native libraries, and kernel The latter data can prove to be very useful if your bottleneck is IO. Using hardware performance counters for sampling is another perf superpower, but this topic is extensive enough to be explored in a stand-alone article. - [Liberica JDK 18.0.1, 17.0.3, 11.0.15, and 8u332 builds are generally available](https://bell-sw.com/announcements/2022/04/21/liberica-18-0-1-17-0-3-11-0-15-and-8u332-builds-are-generally-available/): Today we announce a Critical Patch Update (CPU) of Liberica JDK. CPU patches (versions 8u331, 11.0.14.1.1, 17.0.2.1, and 7u341) contain fixes for Common Vulnerabilities and Exposures (CVE) and help to keep the runtime secure and performant at all times. In addition to the CPU, we also release Patch Set Update (versions 18.0.1, 17.0.3, 11.0.15, and 8u332) with non-critical fixes. The release contains 604 fixes and backports overall. BellSoft participated in eliminating 52 issues (31 in JDK and 21 in FX) in all releases. Contents How to keep your runtime secure The summary of fixes List of security issues fixed Summary of fixes in Liberica JDK Upstream changes: highlights Supported platforms Enjoy the most stable runtime! Useful links How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 6 security issues (CVEs) fixed; 86 total security fixes in CPU release: in Liberica 7u341: 17 security fixes, in Liberica 8u331: 20 security fixes + 3 FX, in Liberica 11.0.14.1.1: 19 security fixes + 3 FX, in Liberica 17.0.2.1: 21 security fixes + 3 FX. In addition, PSU releases include a total of 518 bugs and backports fixed: in Liberica 8u332: 23 security fixes (20 + 3 in FX) + 41 additional fixes, in Liberica 11.0.15: 22 security fixes (19 + 3 in FX) + 181 additional fixes, in Liberica 17.0.3: 24 security fixes (21 + 3 in FX) + 174 additional fixes, in Liberica 18.0.1: 23 security fixes (20 + 3 in FX) + 30 additional fixes. Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) Liberica JDK 8u332 Liberica JDK 11.0.15 Liberica JDK 17.0.3 Liberica JDK 18.0.1 CVE-2022-21449 7.5 security-libs java.security network low none none unchanged none high none ● ● CVE-2022-21476 7.5 security-libs java.security network low none none unchanged high none none ● ● CVE-2022-21426 5.3 xml jaxp network low none none unchanged none none low ● ● ● ● CVE-2022-21434 5.3 core-libs java.lang network low none none unchanged none low none ● ● ● ● CVE-2022-21496 5.3 core-libs javax.naming network low none none unchanged none low none ● ● ● ● CVE-2022-21443 3.7 security-libs java.security network high none none unchanged none none low ● ● ● ● Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE CVSS 8 11 17 18 CVE-2022-21449 7.5 - - ● ● CVE-2022-21476 7.5 ● ● - - CVE-2022-21426 5.3 ● ● ● ● CVE-2022-21434 5.3 ● ● ● ● CVE-2022-21496 5.3 ● ● ● ● CVE-2022-21443 3.7 ● ● ● ● Upstream changes: highlights This CPU release contains a number of important additions and updates. It also includes the first patched build of a non-LTS JDK 18. Minor releases are rarely used in enterprise development, but if you installed JDK 18 to try out new features, we recommend you to update it to keep your runtime environment safe and performant. macOS/AArch64 Port The most important enhancement is the implementation of JEP 391, which ports the JDK to macOS/AArch64 platform. Since Apple began the transition of its computers from x64_86 to its own ARM-based microprocessors (M1 or Apple Silicon), the demand for the macOS/AArch64 port has been growing among Java developers. Apple M1 chips outperform traditional Intel processors, but the need to use Rosetta 2 translator affected the Java app performance to a certain extent. Now the JEP 391 is backported to the jdk11u-dev and eliminates the need for translator software. We at BellSoft have advocated ARM architecture for a long time. Our engineers contributed to the enhancement of AArch64 port by proposing and implementing JEP 315. Needless to say that our Liberica JDK has run natively on Apple Silicon since the introduction of this architecture. Updated XML Security for Java The java.xml.crypto module, which defines the API for XML cryptography, was updated to 2.3.0. It helps to preserve the security of cryptographic operations at the highest level. The version also matches the Apache XML Security v.2.3.0 (the Apache Santuario project) now. A special note on XSLTC that introduces an upper limit for the number of groups in XPath expressions (-Djdk.xml.xpathExprGrpLimit=10) which may affect products like Apache Solr. If you ever encounter an error such as “JAXP0801001: the compiler encountered an XPath expression containing ‘X’ groups that exceeds the ‘Y’ limit”, this can be solved increasing the limit by setting “jdk.xml.xpathExprGrpLimit” property to X (add -Djdk.xml.xpathExprGrpLimit=X to java options). Added support for ChaCha20 and Poly1305 to SunPKCS11 provider The cryptographic interfaces in Java (JCA and JCE) are provider-based. SunPKS11 is a provider that serves as a link between the JCA/JCE APIs and the Cryptographic Token Interface Standard (PKCS#11). The support of ChaCha20 (stream cipher) and Poly1305 (authenticator) cryptographic algorithms will be a valuable addition to the SunPKC11 provider in JDK 11. Jline upgrade The Jline library handles the console input and is similar to Zsh Line Editor in terms of functionality. The Jline upgrade to version 3.20.0 will include the support for the rxvt terminal and some general updates. Added support for RSASSA-PSS signatures in OCSP Response The updated Liberica JDK versions allow RSASSA-PSS signed OCSP responses to be correctly verified. Former versions may have thrown exceptions whenever an attempt to verify an OCSP response signed with the RSASSA-PSS signature was encountered. Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK Useful links [JDK-8253795] macOS/AArch64 Port - Java Bug System [JDK-8275082] Update XML Security for Java to 2.3.0 - Java Bug System [JDK-8255410] ChaCha20 and Poly1305 support - Java Bug System [JDK-8274892] Upgrade Jline to 3.20.0 - Java Bug System [JDK-8274471] Support for RSASSA-PSS - Java Bug System - [Insufficient video memory causing NullPointerExceptions in JavaFX apps](https://bell-sw.com/announcements/2022/04/26/insufficient-video-memory-causing-nullpointerexceptions-in-javafx-apps/): As part of the series of short articles by BellSoft, we will look into various exceptions, their root causes, and elimination. Our first candidate is a specific issue causing multiple NullPointerExceptions (NPEs) in JavaFX applications, i.e., insufficient video memory. Problem A working JavaFX application starts throwing multiple NPEs but doesn’t fail. For instance, take a look at these stack traces: NPE stack trace No.1 java.lang.NullPointerException: Cannot invoke "com.sun.prism.RTTexture.contentsUseful()" because "this.txt" is null at javafx.web@19-internal/com.sun.javafx.webkit.prism.RTImage.getTexture(RTImage.java:99) at javafx.web@19-internal/com.sun.javafx.webkit.prism.RTImage.getGraphics(RTImage.java:74) at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCBufferedContext.getGraphics(WCBufferedContext.java:65) at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCGraphicsPrismContext.getPlatformGraphics(WCGraphicsPrismContext.java:127) at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCGraphicsPrismContext.isValid(WCGraphicsPrismContext.java:132) at javafx.web@19-internal/com.sun.webkit.graphics.WCRenderQueue.decode(WCRenderQueue.java:107) at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCRenderQueueImpl.lambda$flush$0(WCRenderQueueImpl.java:46) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305) at javafx.graphics@19-internal/com.sun.javafx.tk.RenderJob.run(RenderJob.java:58) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at javafx.graphics@19-internal/com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:126) at java.base/java.lang.Thread.run(Thread.java:833) and NPE stack trace No.2 java.lang.NullPointerException: Cannot invoke "com.sun.prism.RTTexture.makePermanent()" because "rtt" is null at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCGraphicsPrismContext$Layer.(WCGraphicsPrismContext.java:1370) at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCGraphicsPrismContext$ClipLayer.(WCGraphicsPrismContext.java:1449) at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCGraphicsPrismContext.setClip(WCGraphicsPrismContext.java:344) at javafx.web@19-internal/com.sun.webkit.graphics.GraphicsDecoder.decode(GraphicsDecoder.java:230) at javafx.web@19-internal/com.sun.webkit.graphics.WCRenderQueue.decode(WCRenderQueue.java:97) at javafx.web@19-internal/com.sun.webkit.graphics.WCRenderQueue.decode(WCRenderQueue.java:111) at javafx.web@19-internal/com.sun.javafx.webkit.prism.WCRenderQueueImpl.lambda$flush$0(WCRenderQueueImpl.java:46) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305) at javafx.graphics@19-internal/com.sun.javafx.tk.RenderJob.run(RenderJob.java:58) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at javafx.graphics@19-internal/com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:126) at java.base/java.lang.Thread.run(Thread.java:833) Root cause At first glance it may be difficult to understand that the problem is caused by video memory because NPE is a versatile exception. But there’s one distinctive feature, namely textures mentioned in the message (Texture, RTTexture). It means that NPEs are caused by insufficient video memory like in the cases above. The application continues functioning and allocating objects as the heap is big enough, but there are no more resources for textures. Such objects don’t get initialized, and so when the app tries to access them at runtime, we get NullPointerExceptions. Solution The solution is quite simple, i.e., you need to allocate more VRAM to the application. VRAM or video RAM is a memory type used for storing the data, which is processed by the GPU to render images on display. How to check the amount of VRAM available on your machine? For Windows 10+ users. Go to Settings > System > Display. Select Advanced Display, then click on Display Adapter Properties. You will find the available VRAM under the Dedicated Video Memory. For Mac users. Check the Graphics/Displays box under System information. By default, the allocated max video memory in JVM is 512MB. But you can customize the JVM settings with the -Dprism.maxvram parameter by using a command-line flag java -Dprism.maxvram=2G -jar yourApp.jar Just don’t exceed the physical video RAM size, or else by solving one issue you’ll get a bunch of new ones. For instance, if you set the target VRAM value too high, it may overpower the system and cause the application to shutdown. Furthermore, consider using integrated graphics cards. They are extremely convenient as they keep the textures in the standard memory, where you can set the value for the allocated memory pool as high as you need. If the VRAM volume is still not enough, you can’t do much to increase it except for upgrading the graphics card because VRAM is an integral part of the GPU. Alternatively, use CPU rendering, but it is not nearly as good as GPU rendering because CPU is not specialized for graphics processing. - [Liberica Native Image Kit 21.3.2 and 22.1.0 builds are released](https://bell-sw.com/announcements/2022/04/28/liberica-native-image-kit-21-3-2-and-22-1-0-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 21.3.2 and 22.1.0. Being part of the security update release cycle, the builds contain a number of fixes and important enhancements. Liberica Native Image Kit is a GraalVM-based tool that helps to convert JVM-based applications into native executables. Liberica NIK can be a great addition to your project as it minimizes resource consumption, accelerates the startup time of applications, and supports a wide range of platforms and languages. Moreover, the developers using Spring Boot can create native images seamlessly through a Cloud Native Buildpack that includes support for Spring and Liberica NIK. Summary of fixes and enhancements Linux-AArch64 version now works on Redhat, CentOS, OEL and other Linux distros that have 64K page size. --allow-incomplete-classpath option is deprecated. What it did was basically link at image run time, which is now the default. To link at image build time, use the new --link-at-build-time option. Arguments, when specified, are package names or fully qualified class names. Used without arguments, it requires all classes to be present at image build time. Implemented incremental, concurrent heap scanning during points-to analysis. This makes native image build times shorter. Optimize for Build time (-Ob) option to native-image that performs fewer optimizations, and disables method inlining. Added support for the following JFR events: GarbageCollection, GCPhasePause, SafepointBegin, SafepointEnd, ExecutionSample. A special Feature looks for vulnerable log4j libraries in native images and produces a warning if one is found. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Boost your skills in native image technology If you are only getting acquainted with native images, you can refer to our series of articles that explain how to use Liberica NIK with the most popular Java frameworks: Generate a native executable with Spring Native — a version of Spring framework with built-in support for native images compilation Create a microservice with Micronaut framework, designed especially for facilitating microservices development Build a native image with Quarkus framework that natively supports GraalVM and MicroProfile APIs Conclusion BellSoft reaffirms its commitment to the goal of providing Java developers with a full stack of secure, performant, and affordable technologies. Liberica Native Image Kit will bring the development at your company to a new level: create tiny efficient containers and enjoy the support from a major OpenJDK contributor. So go ahead and download the latest version of Liberica NIK! Download Liberica NIK - [What is RISC-V and is there a Java port?](https://bell-sw.com/announcements/2022/05/18/what-is-risc-v-and-when-is-the-java-port-coming/): When you hear “processor architecture,” what are the first things that come to mind? Most likely, x86 and ARM. But now there is a new player in town — RISC-V. What is it? How does it differ from ARM? How did it manage to evolve from a project created by a group of university researchers into a thriving platform supported by such tech giants as Google, Alibaba, Huawei, and even Intel? And is there a Java port? Find the answers in our article! Table of Contents RISC-V architecture and history What makes RISC-V good RISC vs CISC Open-source nature of RISC-V RISC-V adoption across industries RISC-V vs ARM RISC-V and Java Conclusion RISC-V architecture and history RISC-V (pronounced as “risc-five”) is an open-source instruction set architecture (ISA) based on the reduced instruction set computer (RISC) model. ISA is an abstract computer model, which defines basic operations and describes how to use them, i.e., instructions. These instructions act as a communication bridge between hardware and software. RISC-V includes 47 base instructions, and additional extensions can be added in a modular way. The RISC-V project was created in 2010 at the University of California, Berkeley, by a group of engineers led by Krste Asanović and David Patterson. In 2015, the RISC-V Foundation was founded to stimulate the interest in RISC-V among commercial users, and the project authors transferred their rights to the Foundation. The RISC-V Foundation was later renamed RISC-V International. The project rapidly gained popularity among researchers and IT companies all over the world, and now the nonprofit organization has more than 2,000 members and such partners as Google, IBM, NVIDIA, Western Digital, etc. In 2018, the organization declared a collaboration with the Linux Foundation. But the absolute highlight happened in 2022, when Intel joined RISC-V International and announced that it is investing $1 billion in the RISC-V ecosystem, the biggest expenditure on the project so far. So what is the secret behind RISC-V’s success? What are the prerequisites for its brilliant future? Let’s find out! What makes RISC-V good RISC vs CISC To understand what makes RISC-V a revolutionary technology, it should first be noted that initially, there were two types of microprocessor design: RISC and CISC (complex instruction set computer). In CISC, one instruction set executes several low-level operations (loading, evaluation, data storage). Hence, a program uses fewer specialized instructions, but the number of cycles per instruction is higher. A typical example of CISC architecture is x86. RISC, on the other hand, uses a smaller customized set of instructions, so each program will utilize several instructions, but each instruction is executed on one clock cycle. The execution of several one-cycle instructions takes less time thus enhancing the CPU performance. In addition, pipelining with RISC is more efficient because the initial idea was that each instruction needs equal time to execute. This is not the case for the complex instructions, but the original strategy is implemented in simple hardware. Finally, developing RISC processors is easier and less expensive thanks to a simple instruction set and smaller chips. Pure RISC and CISC architectures are a thing of the past. Right now, all architectures are RISC-like, even x86, which decodes CISC into RISC-like micro-operations. Open-source nature of RISC-V The difference between CISC and RISC is clear, but what exactly is RISC-V? Is it just a fancy name for RISC? Not exactly. RISC-V is an open and free architecture model. It has all the advantages of RISC enhanced by its open-source nature: It is modular, hence flexible. Developers use modules to create any type of device, from compact embedded systems to powerful computers. It boosts innovation. There’s no need to pay royalties for using RISC-V design in research and development, which makes it possible for any enthusiast to contribute to its improvement. It reduces costs of development and speeds up time-to-market. The reuse of open-source intellectual property (IP) enables fast and cost-effective development of software and hardware. There is no single commercial company that rules RISC-V development and production, as opposed to x86, where Intel is almost a monopoly. RISC-V International is an NGO that unites various companies committed to the improvement of this microprocessor architecture. RISC-V adoption across industries According to the RISC-V Market Analysis conducted by Semico, the annual growth rate of RISC-V cores will reach 160% and capture a >14% (approx. 80 billion CPU cores) share of the semiconductor market by 2025. RISC-V advancement is gaining momentum as these processors get adopted by more industries every year. Thanks to their flexibility, high performance, and scalability, they can now be found in any type of device and category, including: Internet of Things Automotive Machine learning and artificial intelligence Telecom Cloud servers and data centers Edge computing Personal computers, supercomputers, microcontrollers This list proves that RISC-V is a working alternative to ARM and x86 proprietary solutions. What is more, RISC-V International provides all kinds of support to its members: online learning, university labs, compatibility suites and tests, events for developers and industry players, as well as an online marketplace. In addition, boards for developing RISC-V applications have become widely available, for example, SiFive Board. Furthermore, the RISC-V Software Ecosystem (RISE) project was founded by industry leaders to complement the efforts of RISC-V International and accelerate the development of software for RISC-V. So RISC-V is not just architecture. It is a thriving ecosystem, which is predicted to shape the following decades of computing innovation by means of open cooperation. RISC-V vs ARM Both RISC-V and ARM are based on the RISC architecture model as opposed to x86. To note: there is also AArch64, a 64-bit extension of ARM architecture since ARMv8-A. Although ARM still holds a dominant position in the RISC market, it faces serious competition from RISC-V. The table below outlines the major differences between the two. ARM RISC-V Business model ARM is based on a proprietary IP model, which means that you have to pay royalties to integrate an ARM processor into your device. RISC-V is open-source and free, so anyone can use it for research and development without paying royalties or violating any patents. Ecosystem As ARM has been around since the 1980s, it has a large community, a lot of libraries, and supports a wide range of software. RISC-V is a relatively new technology, but its community and technical support is rapidly growing. Power consumption According to the tests, the average ARM power consumption is around 36.8 milliWatts. The same tests show that in specific cases, RISC-V consumes three times less power than ARM. Customization ARM has recently started integrating standardized instructions for custom coprocessors, i.e. the instruction set is defined but its implementations may be adjusted. This is similar to how Java interfaces work. RISC-V includes a set of basic instructions and enables engineers to use standardized or custom extensions they need for their design. RISC-V and ARM comparison In summary, the key difference between ARM and RISC-V is the business model. The open-source approach to RISC-V development opens the door to experiments, innovation, and rapid improvement. RISC-V and Java BellSoft, as a major contributor to OpenJDK, welcomes all things open-source. This approach has already proven itself with Java by making it one of the most popular, secure, and rapidly evolving programming languages. We predict a similar future for RISC-V. The OpenJDK community shares our optimism, and its members have been working on the Java port to Linux/RISC-V within the RISC-V Project. The port was integrated into JDK 19 (JEP422). Huawei was the author of the port, which was completed and published in their repository in May 2021, and then became the basis of the current RISC-V project. The port currently supports the general-purpose 64-bit RISC-V configuration (RV64GV). This configuration includes useful instruction sets for Java types implementation: vector operations, floating-point arithmetic operations, atomic operations, base integer instruction sets, etc. Several HotSpot subsystems are also included: The template interpreter The C1 (server) JIT compiler The C2 (client) JIT compiler All current mainline Garbage Collectors, including ZGC and Shenandoah Moreover, experimental support was added for the following extensions: RVV, RVC, Zba, and Zbb. They need to be explicitly enabled in JVM through the following options: -XX:+UseRVV -XX:+UseRVC -XX:+UseZba -XX:+UseZbb The Java port is a valuable addition to both ecosystems because the range of RISC-V hardware is rapidly increasing, and there is a high demand for software support. In addition, all OpenJDK vendors will take part in porting future Java changes to this ISA model. BellSoft goes even further as far as the Java and RISC-V friendship is concerned. We have a special version of our Java runtime, Liberica JDK for Embedded, which is currently optimized for embedded systems working on ARM32 and ARM64. As RISC-V is widely used in embedded devices, we are planning to add the support for this microprocessor architecture, and both platforms will become a powerful development combo. Conclusion As it turns out, RISC-V and Java have a lot in common. They are both open-source, aimed at platform independence, free, and help to avoid vendor lock-in. In addition, they have a community committed to constant innovation through collaboration. So we might expect that this combo will bear very interesting and powerful solutions in the future. - [Java: 27 years of innovation](https://bell-sw.com/announcements/2022/05/24/java-27-years-of-innovation/): When Java was created in 1995, few could imagine that in 27 years, a third of developers would be using it as their main language. The most popular programming languages back then were C, C++, Fortran, and Pascal. It took Java only five years to take the lead and stay on top to the present day. We have decided to congratulate Java on its birthday and pay homage to it by highlighting the best moments in its past and present, and taking a look into its future. The early age Java was born in 1991 as part of the Green Project at Sun Microsystems. Three people initiated the project: Mike Sheridan was responsible for business development, Patrick Naughton worked on the graphic subsystem, and James Gosling developed the design. The language was called “Oak” after the tree outside the office window. The language was originally developed for interactive TV, but at some point, half of the team left the project for Silicon Graphics, and the language itself seemed too innovative and ahead of its time. In 1992, the team demonstrated the first platform prototype and then focused on a PDA device *7 (Star Seven). The language called Oak already existed so the new one was renamed into Java. The Mosaic browser was launched in 1993 and transformed the world of Internet technologies. So the Green Project team decided to create a browser written in Java only and released it in 1994 under the name of HotJava. Java itself was officially presented at the Sun World conference on May 23d, 1995. Java 1.0 came out in 1996. Today, after 18 releases, Java surpassed even the boldest expectations! The prime of life What is the secret behind Java’s success? Clear and convenient syntax Strict typing All-encompassing and reliable standard library Platform-independence Formal specification Maybe the most important secret is Java’s open-source nature and large community that enables its rapid evolution. Thanks to all these advantages Java is utilized in multiple fields of work like these: Online services and social networks Twitter, Facebook, Netflix, Spotify, and many other companies whose products are used by millions of people globally utilize JVM and Java. There are various reports on how exactly they integrate Java into their workflows: have a look at the presentations by Chris Thalinger from Twitter or Josh Evans from Netflix. Enterprise If you are using an enterprise portal or bug tracker in your line of work, there is a high chance it is written in Java. The most popular ones are Jira and YouTrack. Companies are utilizing Java for internal development, for instance, in Business Process Management (BPM) or Enterprise Asset Management (EAM). Modern frameworks such as Spring and MicroProfile are very convenient and extremely popular among developers because they provide a comprehensive platform for writing various applications, including enterprise ones. Java is supported by all key cloud providers: Amazon, Google Cloud, Microsoft Azure, and is also suitable for implementing tools and best practices of DevOps. Desktop applications and IDE Regardless of the preferred programming language, most developers are well acquainted with development environments: IntelliJ IDEA, Eclipse IDE, and NetBeans. They are created utilizing frameworks such as Swing and SWT. Thanks to concise and excellent architecture and simple development process, we can develop plugins for them and add support of favorite technologies. Big Data Hadoop, Spark, Flink, and Storm are based on the JVM languages. In 2022, Java, Scala, and Python were named the most popular languages for developers working with big data. Scientific calculations and engineering If your work involves mathematics, you must have heard about or are already using MATLAB or Maple. These solutions utilize Java both for graphic interface and backend. The European Organization for Nuclear Research (CERN) also maintains several Java projects and GitHub organizations. Video games In 2019, Minecraft Java Edition became the best-selling video game of all time despite it being created in 2008. There are not many games with graphics developed in Java, but it is a commonly used language for server backend, for instance, in Blizzard and Electronic Arts. Internet of Things and Embedded Last but not least, Java is one of the most convenient software solutions for hardware. One can create amateur projects with smart light switches, experiment with robotics, integrate cloud solutions such as Amazon IoT Platform, and develop many other interesting technologies. A lot of IoT projects were developed with support from Eclipse. A never-ending story These advancements of Java in the world of programming would be impossible without an active community. Thousands of developers and companies contribute to Java development thus shaping the Java ecosystem we know. We all enjoy the benefits of the OpenJDK project, Java frameworks, and specifications such as Spring and Jakarta. Businesses can form a comprehensive technology stack based on Java, which simplifies the development process and accelerates time-to-market. There are many new OpenJDK distributions that appeared in recent years. They are supported by major Java contributors: Oracle, BellSoft, Azul, Amazon, and others. New enhancements are integrated into each Java version. The release cycle has been shortened, new versions come out twice a year. Therefore, Java is characterized by constant improvement, with new innovations such as Amber, Leyden, Loom, Panama, Valhalla, ZGC on the way. Between JDK 8 and JDK 18, more than two thousand changes were introduced into garbage collection only. Container and cloud support has also been enhanced. At the same time, Java is a secure language because it maintains a balance between rapid innovations and adherence to traditions. Therefore, it provides developers with great compatibility, performance, and reliability. We believe that 27 is not the autumn of Java’s life, but spring with many years of breathtaking improvements and innovations ahead. Happy birthday, dear Java! - [Role of a database in TCO reduction](https://bell-sw.com/announcements/2022/05/26/role-of-a-database-in-tco-reduction/): The strategy of reducing the TCO of Java applications includes numerous items, and today we take a closer look at one of them — a database. How a database affects TCO Growing workflows, long-term archives, and backup copies for emergency retrieval produce huge data volumes. A database greatly simplifies the life of your team (assuming that it is set up correctly), but how exactly does it affect the overall costs? Storage resources. If not allocated and managed correctly, data can devour the resources. A relational database grows exponentially, data storage and retrieval consume more CPU. Huge tables need more time to process queries thus slowing the application down. In addition, to manage the growing traffic, you need to scale the DB vertically by adding more storage capacities, so the investment inflates. Licensing. Some established DBs are proprietary software that comes with license or service fees. Prices tend to differ and are often hard to calculate (especially in the case of service fees) as DB vendors may charge for server time or data volume. Administration. DB management requires a competent expert, sometimes a dedicated team, so the expenses for database administration personnel can be a significant part of total TCO. Compatibility. Platform-dependent DBs require separate deployments for various configurations and that, in turn, spikes costs and development time. Database selection: key points To begin with, not every application needs a database. If you have a stateless service and don’t need to handle large bulks of information, you can store your data locally in files. This way, you will accelerate the work of your application, simplify the recovery, and reduce database management costs to a zero. Secondly, do not be afraid to utilize open-source tools. The OpenJDK project demonstrates that open source can be as reliable, secure, and performant as proprietary software. There are many great open-source databases: MySQL, PostgreSQL, MongoDB, Redis, Cassandra, etc. Most of them are well-established and have great support teams. So browse a wide selection of solutions on the market, analyze the feedback on their support, and choose a DB that best fits your business model. Thirdly, consider embedded Java databases. Embedded Java databases like Daffodil DB carry an enormous potential in reducing the overall TCO: They are platform-independent and so eliminate any compatibility issues and can be used as unified tools They are embedded with an application and run on the same JVM as your app. So the database performance is tuned together with other JVM settings, which minimizes administration expenses Last but not least, you can migrate your data to the cloud. Amazon AWS offers various purpose-built data stores: NoSQL, Big Data, Caching, Object storage. By shifting away from conventional relational DBs and taking a more flexible approach to data storage, you get better data availability and reduced infrastructure costs. To sum up, a database shouldn’t be neglected when planning a TC reduction strategy. But, as usual, there’s no one-size-fits-all solution as everything depends on your business needs and capacities. Want to discuss a topic of TCO reduction in more detail? Contact us, and our engineers will be glad to help you on the matter. Book a free consultation - [Optimizing cloud costs](https://bell-sw.com/announcements/2022/06/01/optimizing-cloud-costs/): Find out the reasons for sky-rocketing cloud expenses Reducing cloud costs is a hot topic in the modern IT world. In 2021 managing cloud computing expenses was one of the top three major enterprise issues along with enhancing security and governance. 61% of companies set cloud cost optimization as their top priority1, and this trend is bound to continue. In this article, we will learn how to identify the root cause for suboptimal app performance and inflated resource consumption. Why are your cloud bills so high? Default containers are black boxes Underutilization and overutilization: two sides of one coin SLO or SLA? Both! Cloud monitoring: life after deployment Get Kubernetes metrics Analyze GC data Measure app performance under load Retrieve and understand VM statistics Conclusion Useful links Why are your cloud bills so high? The realization that your company spends tremendous amounts of money on cloud resources usually comes out of a clear sky. The application demonstrated optimal peak performance on bare metal, was conveniently containerized, sent to the cloud, and scaled, the SLA was met — what could go wrong? The answer is “everything”. Several problems could be revealed even without delving deep into the metrics: Developers are not directly involved in cost optimization. They are not aware of the execution environment and write the code accordingly. At the same time, EKS administrators or cloud algorithms may not take application characteristics into consideration or even be aware of them. As a result, this awareness gap becomes an abyss for corporate finances. An SLA provides the roadmap and the warranty for your project in the cloud. However, if a cloud services consumer doesn’t examine the SLA carefully or validate it against worst-case scenarios, there is a risk of running into unexpected costs if something goes wrong2. If the application performs well on bare metal, it doesn’t mean that the same performance can be expected under different loads in a cluster. As a result, pods shut down unexpectedly or HotSpot devours much more memory in the same instance. Not all services are the same, some of them are less critical than others. However, unloaded services consume the same amount of resources as the loaded ones if the same scaling strategy is applied to them. Developers build applications in containers using default settings. As a result, you get a container that doesn’t meet your business needs, takes a lot of space, or underperforms. Let’s examine the last two points more closely. Default containers are black boxes Paketo Buildpacks, the most popular buildpacks for containerization, Cloud Native Buildpacks, and other similar technologies offer an opportunity to create container images directly from Maven or Gradle plugin. Developers generate small and performant containers with a couple of clicks thus reducing development time dramatically. However, they rarely configure containers themselves and prefer to use default settings, because if an automatically generated image performs well, why bother? The problem with out-of-the-box solutions is that they are, in fact, black boxes, i.e. the developers don’t always know how these solutions work and thus pick ones that don’t meet business requirements. As a result, Paketo containers with default settings, for example, may underutilize memory and CPU. If you want to have highly performant, secure, resilient, and small containers, you have to configure them manually: Start with JVM tuning and optimize the setting such as -XX:+AlwaysActAsServerClassMachine or -XX:+PerfDisableSharedMem. You should also select the appropriate Garbage Collector Perform testing such as load testing, A/B, longevity testing, plan the capacity. Load test results should match the “no container” case Keep in mind that every container in the pod must have a memory limit and a memory request Align resource requirements with possible node sizing for correct node scheduling Technical overhead might be small, but don’t forget to take the K8soverhead into account. For example, Fargate uses extra 256MB RAM Consider using Liberica Lite and Alpaquita Linux to minimize the size of your containers without affecting the performance or security After the deployment, you need to perform continuous health monitoring, daily load testing, and control the metrics: latency, throughput, GC statistics, etc. Find a more detailed description of metrics monitoring in the section “Life after deployment” below. As you can see, containers require your attention and care. To summarize this section: automatic settings are convenient but suboptimal, so if you want something done right, do it yourself. Underutilization and overutilization: two sides of one coin Technically, all services are created equal, but incorrect memory allocation settings lead to overutilization or underutilization. In case of overutilization, the service is loaded to the limit and consumes a lot of resources, therefore The application doesn’t function properly but performs continuous garbage collection instead. We have to add one more instance, which in turn automatically doubles the expenses. As far as underutilization is concerned, the application doesn’t use all the available memory, which leads to the same issues: continuous GC usage and the necessity to allocate one more instance instead of consuming the existing resources. These are two different problems with the same root cause: you have a memory limit that doesn’t correspond to the functions of your application. SLO or SLA? Both! We have already brushed upon the SLA between you and your cloud provider in the first section. Now it’s time to talk about your obligations towards customers and/or users. Basically, an SLA is an agreement between a company and a paying user, so if your services are distributed for free, you don’t need an SLA. If that is not the case, make sure that not only lawyers, but your technical team takes part in the process of creating SLAs. Only developers know what it takes in terms of time and resources spent to deliver the services as per SLA in the case of a cloud-based application. The same goes for SLOs. If an SLA is an agreement between a company and a customer, an SLO (service level objective) describes objectives that your developers have to satisfy in order to meet the SLA. SLOs should be precise, realistic, and tightly bound to the SLA. Make an effort to choose the essential metrics, analyze and spell out the objectives with your team, and include realistic and worst-case scenarios into SLOs and the SLA. Cloud monitoring: life after deployment We have summarized the most common issues that lead to high cloud expenses. Suppose you already have a running application in the cloud and yet you aren’t sure where to look to hunt down the root causes. What exactly is there to blame: JVM, the app, containers, the SLA? Read on to learn about the metrics that provide you with all the necessary data for the subsequent optimization. Get Kubernetes metrics Start with retrieving the Kubernetes metrics to get a clear understanding of what is going on in your clusters. For that purpose, you can use open-source tools such as Metrics Server, and various kubectl commands. First, install the Metrics Server. There is a chance it is already deployed in your cluster, so you can check that by running kubectl get pods --all-namespaces | grep metrics-server You will get a response with a list of running pods if the Metrics Server is running. Otherwise, run the following command to retrieve the latest version of the tool: kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml Metrics Server allows you to collect the data on the availability of resources: CPU, memory, storage, as well as their utilization. kubectl top is a command that allows you to get the metrics from a particular node, pod, or all cluster’s pods or nodes. For example, if you run kubectl top node you will get the current CPU and memory utilization of all nodes. The same applies to kubectl top pod The command kubectl top pod --namespace=kube-system –-container will return the resource utilization in a pod distributed among containers. The command kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes/ | jq or kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces//pods/ | jq will return the Metrics API in a JSON format from a pod or a node. Another useful tool is kube-state-metrics, which return the state of nodes and pods: status, capacity, amount of replicas as per deployment, etc. Finally, you can use kubectl logs to retrieve the detailed information on pods or nodes. Analyze GC data Java applications perform memory management by means of a Garbage Collector (GC). GC tuning can have a significant impact on performance. But first, you need to get a clear understanding of what is going on with garbage collection in the application. For that purpose, GC logs are of great help. To begin with, enable GC logs: -Xlog:gc*::time After that, you can aggregate these logs using the centralized log management and APM systems or analyze individual logs. GC logs are generated in a text format and can be processed by scripts and desktop and online GC log analysis tools. JFR is another useful monitoring tool. We have already discussed how to use it to pinpoint memory issues and identify the root cause of Stop-the-World pauses. JFR records GC events that can be processed by JDK Mission Control or APMs, and there are also simple JMX indicators. GC throughput is a ratio of application time spent doing business logic as compared to the overall time, which includes GC and other JVM service work. The CPU resource is utilized properly when CPU load is close to 100% and GC throughput is close to 100% too. Measure app performance under load Remember that applications may behave differently in the cloud and under high loads. Never rely on baseline performance and know the CPU and RAM limits and requirements. In summary, you should do the following: Perform load testing to understand the app’s behavior under both normal conditions and possible peak load Perform longevity testing to validate the stability of the application and its consistent quality Identify peak rates and latency. Any system may fail unexpectedly, but you should try to prevent it. Keep in mind that peak rates assume high resource utilization Identify CPU and RAM requirements per service Use mock services to simplify the testing and give your team reference points to go by Retrieve and understand VM statistics JVM tuning involves GC optimization, but is not limited to it. In short, JVM performance tuning is a complex task, which will be the subject of a separate article. But before optimizing any settings, you have to learn how to retrieve JVM metrics and perform JVM monitoring. It is possible to collect performance metrics observed on a client by using stress load tools like JMeter or wrk, or to study inner metrics captured on back-end and collected by the APM. They can be collected at various levels: machine JVM web server, BigData system, message queue, etc. application Latency is often critical for customers. It is measured and targeted at certain percentiles (50%, 95%, 99%, 99.99%, 99.9999%). So it can be additionally studied at different angles like overall application lifetime or lifetime after reaching peak performance. Conclusion In this article, we learned what metrics you should use to identify the problems with resource consumption in your cluster and how to retrieve them. The most important message is: no default settings, algorithms, or automation tools help you optimize the resource consumption the way you desire. You have to get your hands dirty to optimize the containers and configure the JVM yourself. If after reading the article you feel bewildered with the amount of work that lies ahead of you, book a free consultation with a BellSoft engineer. Together we will walk through the optimization hardships to reach for the stars! Contact our experts Useful links 26 cloud computing statistics, facts, and trends for 2022 Lightning hits Dublin data centers - [JEP 428: Structured Concurrency (Incubator)](https://bell-sw.com/announcements/2022/06/03/jep-428-structured-concurrency-incubator/): The OpenJDK constantly works on improving the Java language. The new JEP 428: Structured Concurrency, which is now an incubating API, is aimed at simplifying multithreaded programming. Justification Concurrent execution of subtasks belonging to one task helps to reduce latency and speed up the operation process. In Java, java.util.concurrent.ExecutorService is commonly used for concurrency operations. But the ExecutorService doesn’t understand the task-subtask relationship, which means that If a subtask throws an exception, other subtasks may not notice that and continue working, leading to thread leaks If a task fails, subtasks continue running. It will also lead to thread leaks A task may not notice that a subtask has thrown an exception and will wait for other subtasks to execute instead of canceling them It is possible to structure the code manually by introducing try-finally and try-with-resources, but writing such code is time-consuming. Therefore, there is a need for reliable and automatic concurrency structuring. Goals JEP 428 introduces a library for structured concurrency that treats multiple tasks from different threads as one unit. The task is split into subtasks, which return to the same code block nested in the block of a parent task. The parent task monitors the subtasks and waits for their result. This way, a tree-shaped hierarchy of tasks is built at runtime, with sibling subtasks being owned by a parent task. Such a hierarchy can be reified just like a call stack. Structured concurrency allows to define clear entry and exit points of operations coming through the code block and brings the following benefits to the developers: It enables reliable coordination of virtual threads. Virtual threads are lightweight and can be used simultaneously in large numbers. Structured concurrency enables the processing of millions of virtual threads by dedicating threads to tasks and their subtasks. It enhances the reliability of multithreading by enforcing the delimitation and lifetime of operations: the lifetime of all concurrent sibling subtasks is confined within one syntactic block. Each subtask returns to its parent, and no subtask can finish after the task. If the parent notices the exception thrown by one subtask, it can cancel all other subtasks. Structured behavior of subtasks reporting only to their parent helps to prevent thread leaks and cancellation delays. Conclusion We are looking forward to this change as it may prove significant in the case of multithreaded server applications. If JEP 428 is introduced in JDK 19 as a preview, developers can test structured concurrency with virtual threads. As for now, you can read more about this feature on the OpenJDK website. - [11 reasons to start using containers now!](https://bell-sw.com/announcements/2022/06/08/11-reasons-to-start-using-containers-now/): The popularity of containers grows proportionally to the adoption of cloud-native development. According to Gartner, more than 75% of companies will be using containers by the end of 2022, a significant increase compared to 30% two years ago. Are you still stuck with virtual machines? Discover 11 reasons why you should have started containerizing your applications yesterday, from lowering the total costs to simplifying the life of your developers! Or contact our experts to find out about the most performant microcontainers for Java applications. Get Free Advice 11 cases when you should use containers You deploy to the cloud You need to lower the TCO You write microservices You need unified testing and prod environment You have a hybrid cloud application You want to enhance DevOps practices You want to use third-party software You want to speed up development and testing You want to simplify the build process You want to unify the tech stack You need to reduce the provision time of your application Conclusion 11 cases when you should use containers We have already discussed the differences between containers and VMs, as well as the benefits containerized applications bring to the business. This time we focus on key container use cases. Take a look — there is a high chance you will find some of these situations quite familiar to you! 1. You deploy to the cloud Whether you want to migrate an existing application to the cloud or have started developing a cloud-native solution, containers are great packaging instruments for fast cloud delivery. Key cloud providers such as Amazon AWS or Microsoft Azure support containers and provide services like Amazon Elastic Container Service that simplify deployment, management, and scaling of containerized apps. Instead of mastering multiple virtualization technologies, developers package an application in a standardized way, and cloud administrators know how to run it because containers have a standard API. 2. You need to lower the TCO Containers share the same OS kernel, the host one, and use special network interfaces. Each VM has its own kernel and runs on top of a host machine as a guest OS using a hypervisor. The host (hypervisor) kernel controls hardware (CPU) virtualization and helps to emulate devices such as network adapters. In contrast to VMs, containers are much more lightweight (megabytes vs. gigabytes) and thus consume fewer resources. It is vital for the cloud, where minimized footprint leads to lower cloud bills. But by switching to containers, you will also reduce the number of servers you need to host the application data. So hardware costs get decreased as well. And the smaller the container, the bigger the savings! Find more recommendations on lowering the TCO by choosing the right OpenJDK vendor in our article dedicated to the topic. 3. You write microservices Containers and microservices are a perfect tandem. Containerized microservices are easily isolated, deployed, and scaled. Updating or debugging of services is also simplified: there is no need to take down the whole application. Simply kill a faulty container and substitute it with a fresh one. And as software in a container forms a stack, when you update the upper layers, the lower cached levels are reused, thus making the whole process much faster. We created a guide to different kinds of microservices architecture highlighting their distinguishing features and benefits, so if you are thinking about breaking down your monolith, check it out! 4. You need unified testing and prod environment Tools such as Docker enable the developers to test the application in the same environment it will be deployed. In this case, there will be no unexpected errors in the production related to differences between testing and prod environment configurations. Make this process even more reliable by unifying a runtime for development and testing: this way, you will prevent bugs hidden in the JDK from appearing later on the pipeline. 5. You have a hybrid cloud application Containers are highly portable and can be shifted between independently managed servers and cloud with minimal changes. Developers working in multi-cloud or hybrid cloud environments can containerize an application once and then move it to any machine or cloud. VMs are also portable, but the process is more complicated due to the intricacies of virtualization. A microcontainer of 42 MB enables fast provisioning and minimized push/pull times. 6. You want to enhance DevOps practices Containers promote agile development and best DevOps practices by smoothing out and accelerating the CI/CD pipeline. The packaging process is standardized, and the whole development process is much easier to manage. Containers are replicable and can be transferred between teams nurturing cooperation within the company. Moreover, containers fit the “shift left” approach perfectly, as they are allocated to separate microservices that can be enhanced, tested, and debugged by the same team working on their development. Are you still struggling with integrating DevOps into your corporate culture? Read our article about common mistakes made when implementing DevOps — you might be doing it wrong! 7. You want to use third-party software A development process often implies using third-party tools and solutions: runtimes, OSs, databases, utilities, etc. Installation and setup can be complicated and time-consuming, but this is where containers come to the rescue. Many such utilities are available as container images with all the necessary dependencies, running predictably in any environment. Pull an image from a public registry such as Docker Hub, and don’t worry about manual configuration. One thing to note is that you should regularly update the software you are using because fresh images contain fixes for known bugs and vulnerabilities. 8. You want to speed up development and testing Applications need to be tested even at early development stages, and you can use containers for easy testing of a large set of heterogeneous components on a developer’s local machine. Even if you need the other team to perform the testing, developers simply pull the container and run it without wasting time on additional configurations. In addition, containers enable you to roll out the whole cluster of heterogeneous processes emulating a typical service, which is impossible with VMs or nonisolated components. Furthermore, some dependencies you need for the project can also be pulled from a repository in the form of containers. By pulling a container and pointing the application to the running instance, the developers minimize the time spent on manual configuration. 9. You want to simplify the build process Containers also simplify the compilation and build processes. Container images can provide a build environment where you transfer the app code for compilation. In the case of Java, there is a Maven Docker image with Maven build tools inside. Passing the mvn clean install command will result in code compilation and .jar file creation. After that, the .jar file can be injected into another container, which enables leaving out unnecessary dependencies, including the build tools, and keeping the container as small as possible. It is also possible to automate configuration by writing appropriate Dockerfiles, which eliminate manual labor when starting a new container. 10. You want to unify the tech stack The more external software you use, the more vendors you have to work with. Therefore, several issues arise: reliability, timely updates, technical support, licensing costs, etc. A unified technology stack promotes agility and helps to reduce expenses. Containers are a great asset in this regard because they can be configured to contain a unified runtime, OS, and dependencies. Build a container suitable to your goals once and replicate it throughout the company — this way, you will Minimize the number of vendors you work with Simplify software integration Save the time of your architects, developers, testers, and other experts 11. You need to reduce the provision time of your application The speed of application provisioning depends on multiple factors. As far as containers are concerned, containerized applications are provisioned, deployed, and started faster than the VM-based ones because they don’t need the whole OS. Use native image technology to accelerate these processes further. It helps to create small native executables containing application code, necessary libraries, APIs, and a reduced VM, so containers built with native images are even smaller! Conclusion BellSoft believes that containers will be a golden standard of cloud-native development in the near future. So if you are just starting with containers or are planning to, we would like to enhance your experience with this technology. We prepared a special solution for enterprise cloud development: a unified tech stack packed in the smallest container on the market! Intrigued? Subscribe to our newsletter, and don’t miss the news! Subscribe to our newsletter - [Leyden: The new OpenJDK project](https://bell-sw.com/announcements/2022/06/10/leyden-the-new-openjdk-project/): Project Leyden, launched in May 2022, aims to improve the startup time of Java applications, time to peak performance, and resource consumption by utilizing static runtime images. The concept of static images and project goals A static image is a standalone program that runs only an application it is derived from. Static images work under the closed-world assumption, i.e., they cannot load classes outside an image or generate new bytecode at runtime. These features enable the compilation of Java code to native executables, which is similar to GraalVM functionality. Although static images are not a one-size-fits-all solution, they are highly beneficial for cloud deployment: Minimized startup and execution times can lower the cost of cold starts Small images decrease cloud resources consumption Static images will be added to the Java Platform Specification. It is expected that GraalVM will implement this specification too. This way, developers will be able to shift from Leyden to native images or any other conforming implementation to reach the startup or footprint indicators fitting their project perfectly. Project Leyden will be based on the following JDK components: HotSpot JVM The jaotc ahead-of-time compiler Application class-data sharing (AppCDS) The jlink utility Native images are a useful technology for startup and footprint minimization. For instance, Liberica Native Image Kit (NIK), a GraalVM-based tool, helps to accelerate the startup up to 1/10 s. So while Project Leyden is in the makings, experiment with native images and see for yourself that they are a game-changer in Java! Discover Liberica NIK Project Leyden: gradual introduction Mark Reinhold, chief architect of the Java Platform Group at Oracle, states that the project working group will take a step-by-step approach in terms of introducing a closed-world constraint to the Java platform. The group will start with analyzing various constraints, weaker than the close-world-constraint but suitable for a wider range of applications. The long-term goal is to adopt the complete close-world constraint so that developers can produce fully-static images. Project Leyden currently has no repositories as it is at the conceptual stage of development. The repositories with documentation and code will be added gradually as the work on the project progresses, and the project itself will be introduced in a series of JEPs. - [Linux Server and Linux Cloud support is a must and here is why](https://bell-sw.com/announcements/2022/06/15/linux-server-and-linux-cloud-support-is-a-must-and-here-is-why/): Prepare the umbrella before it rains Linux is the most popular operating system for servers and the cloud: it is flexible, versatile, and suitable for a wide range of user cases, from cloud to embedded devices. It is also open-source, just like OpenJDK, which means you can build your custom distribution or use a ready solution without paying a dime. The large Linux community will take care of bugs, security patches, and enhancements so your OS will always be safe, updated, and stable. Or will it? In this article, we will look into a less obvious issue related to software development — Linux support. We will find out why you should treat it the way you treat an insurance policy and how to choose a reliable Linux distribution. What can go wrong with your Linux Server or Linux Cloud distro? Licensing issues Delayed security updates and bug fixes Linux Server and Linux Cloud commercial support nuances Conclusion: unlock the bonus! What can go wrong with your Linux Server or Linux Cloud distro? Licensing issues A standard Linux distribution includes the Linux kernel and additional tools, libraries, and documentation. The kernel and many software components are distributed under the ​​GNU General Public License (GPL), version 2. However, a distro may aggregate proprietary software distributed under other licensing conditions. Using licensed utilities free of charge may lead to litigations or unexpected expenses related to copyright violations. Linux community members may have no evil intention of putting proprietary software into the packages with Linux Server/Cloud distributions, but ignorance is no excuse. In the end, you will be the one to bear responsibility for utilizing licensed components in development and production. Delayed security updates and bug fixes Linux software is developed within a large community whose members dedicate themselves to its enhancement and enforcement. You don’t have to pay for updates because developers work voluntarily. But you are also not guaranteed to receive timely patches for the same reason. There is no responsibility imposed on a specific person. Although the community strives to preserve the max. safety of packages, it is not their primary job. A package maintained by one developer may tomorrow be transferred to another community member or left unattended. Ideally, a Linux Server/Cloud distribution should receive regular updates so that your data is always protected. For example, a CPU release cycle of Java binaries has proved to be an excellent preventive measure against zero-day vulnerabilities and exploits. As far as bug fixes are concerned, there is a procedure for reporting bugs found in the Linux kernel. A user has to identify a subsystem causing the issue and send a report with a detailed bug description to subsystem maintainers via Bugzilla or a subsystem mailing list, which can be found in the MAINTAINERS file. A maintainer usually takes 1 to 5 business days to react to the report. However, the response may take two weeks, depending on the circumstances. If you have a business-critical application or the one handling sensitive data, you can’t afford to wait several weeks for a bug fix: it would be like waiting in line for an umbrella when it is pouring rain. So community editions are not always suitable for enterprise development, which leaves us with an obvious solution — commercial support. Linux Server and Linux Cloud commercial support nuances With the linux-kernel mailing list receiving more than a thousand reports per day, drawing attention to your particular problem may be extremely difficult. All that time, your application will be vulnerable to attacks. The solution is to protect yourself from risks by investing in dedicated support. Commercial support is like an insurance policy: a company will spend much more on recovering from cyber-attacks. The damage also includes hidden costs such as loss of customers and reputation, falling stock prices, etc. So see about protecting all software components you are using, including the runtime and operating system. How will you benefit from high-quality Linux Server/Cloud support? 24/7 or 24/5 online or phone support from engineers developing the product Access to the latest patches and fixes, as well as troubleshooting tools and product documentation Continuous delivery of updates Prompt feedback based on SLA Luckily, some Linux vendors offer commercial support, for instance, Red Hat Enterprise Linux (RHEL), Ubuntu, or SUSE Linux Enterprise Server. What should be taken into consideration when choosing a Linux vendor? Subscription plans vary and may be quite expensive. RHEL Server with premium support costs $1,299 a year and includes 24/7 support for 1 and 2 severity cases and standard support within business hours for other issues. Advanced support for Ubuntu costs $1,500 for a physical server and $500 for a virtual one. Each vendor fixes bugs by priority. For example, Red Hat has a bug tracking system for submitting defects found in Red Hat distributions, so the team works tirelessly on eliminating issues and enhancing their product. It is great for RHEL users in general but may be suboptimal for you. There is a chance you still have to wait for a while for your problem to be solved if a more severe vulnerability is calling for immediate attention. What if you need Linux for the cloud? Amazon developed Amazon Linux 2 for Amazon Cloud, supported through a subscription to AWS Support. But it is not suitable for multi-cloud or hybrid cloud environments as your issues will be solved only within the scope of Amazon Cloud. When choosing optimal support plans, don’t forget about important characteristics of the Linux distribution, such as size. A heavyweight distro will consume more resources and affect cloud costs. Unfortunately, the most lightweight distro, Alpine Linux, goes with community support only. Conclusion: unlock the bonus! Using a free Linux Server/Cloud distribution helps you to lower TCO in the short term, but goes hand-in-hand with the risks of not receiving prompt help when required. A perfect Linux distribution for Java applications is characterized by the following: It is lightweight and suitable for building microcontainers It contains tools for convenient Java development It is supported by a reliable vendor fully responsible for component licensing and timely fixes It would also be better if the vendor provided unified support both for Linux and Java so that you could work with one partner. Does such a distro exist? Actually, yes! BellSoft engineers created a unique solution for your Java applications — Alpaquita Linux. It is a small Linux distro of only 2.9MB in size, with features for enhanced security and performance, security advisory, and 24/7 commercial support from engineers who develop the product. But there is more! Alpaquita Linux is part of Alpaquita Cloud Native Platform, a tiny container with Alpaquita, Liberica JDK Lite, and Liberica Native Image Kit for developing and deploying cloud-native Java applications. We guarantee that ACNP will take your enterprise development to a new level! - [JEP 425: Virtual Threads (Preview)](https://bell-sw.com/announcements/2022/06/17/jep-425-virtual-threads-preview/): JEP 425: Virtual threads is targeted to JDK 19 as a preview API. Virtual threads are part of Project Loom, which has been in the making since 2017. The project is aimed at enhancing the concurrency performance in Java by letting the developer write and maintain concurrency applications with familiar APIs and use hardware resources more efficiently. Virtual threads: overview Motivation Concurrency in Java is handled by threads. Each thread executes its tasks independently of others and provides a stack trace, so it is easy to debug or profile them. There are two methods of working with threads. The first one is thread-per-request: a thread is dedicated to a request for its entire duration. Such a design is easy to write, handle, and debug, but we can’t have too many of them. Threads are implemented as wrappers around operating system threads, which are costly. So to scale an application efficiently, developers sometimes use a second method — asynchronous programming. In this case, threads don’t hold on to requests but rather return to a pool to handle other requests. Such request-handling style allows for a higher number of operations without increasing the number of threads, but makes it difficult to write code and understand the app’s behavior. Solution Virtual threads are the mechanism that enables the developers to preserve the clarity of thread-per-request programming and significantly increase a number of threads. Virtual threads are not tied to OS threads for the whole lifetime of the code. Instead, they capture an OS thread only while they perform their calculations, i.e. multiple virtual threads can share one OS thread. Virtual threads are cheap and plentiful. In a situation when a program must perform 10,000 tasks concurrently, only 200 OS threads can be created, which means the throughput will be 200 tasks-per-second. But the code may easily create 10,000 virtual threads, so the throughput will be 10,000 tasks-per-second. The number of tasks can further be increased, for virtual threads, it doesn’t matter. Therefore, the application can be scaled efficiently with optimal hardware utilization. At the same time, virtual threads are easy to debug, profile, and execute. JDK debuggers and profiles can step through virtual threads, analyze stack traces, and associate code events with the right threads. As virtual threads are part of the JDK, it uses its own scheduler, ForkJoinPool, to schedule the threads and run the code, which doesn’t need to be rewritten. Moreover, there is no need to pool virtual threads, because thread pools are used for expensive resources, and virtual threads are cheap and plentiful. Affected areas The following JDK components will be updated with this JEP: java.lang.Thread Thread-local variables java.util.concurrent Networking java.io Java Native Interface (JNI) Debugging (JVM TI, JDWP, and JDI) JDK Flight Recorder (JFR) Java Management Extensions (JMX) java.lang.ThreadGroup In addition, multiple tests will be conducted before JDK 19 release to ensure there are no unexpected regressions or effects on performance. Main risks associated with the proposal are related to compatibility due to changes introduced to the main APIs. The key risks and assumptions are listed on the JEP’s page. Virtual thread will go well with structured concurrency, a new API for creating and managing threads with clear relationships between them. Project Loom value for Java JEP 425 is a major step towards integration of Project Loom into the JDK. The project will benefit all Java developers as it enables lightweight scalable concurrency as part of the JVM without the need for additional libraries or frameworks. The significance of Loom for the Java language cannot be overestimated. In this article, we provided a short summary of virtual threads, but the project calls for a more detailed discussion in a separate article. Subscribe to our newsletter so as not to miss it! Subscribe to our newsletter - [Creating Java microservices with Spring Boot and Liberica JDK](https://bell-sw.com/announcements/2022/06/20/creating-java-microservices-with-spring-boot-and-liberica-jdk/): A guide to building Java microservices. Part 1 In modern enterprise software development, microservices are becoming increasingly popular. Although this type of architecture is no silver bullet and implementing it is significantly more challenging compared to monoliths, it is the preferred one in many instances. For a deep dive into the theory of microservices, go to our previous article, introduction to understanding the architecture. This post, in contrast, will deal with practical issues. Here I will describe designing and developing microservice architecture through a real-world use case: a cloud-native Spring Boot Java application for an online store based on open source Liberica JDK. Set up the project Unified Tech Stack Implementation Configure the Order microservice Domain/Entity Repository Controllers Configure the Customer microservice Services Conclusion Set up the project Unified Tech Stack I’ll take a JVM-based tech stack for its legendary backward compatibility and being the number one choice in enterprise software development. Stable and reliable Java™ will suit best as a programming language for our purposes. What about the runtime? Oracle has recently introduced a hefty price for Oracle’s JDK use in production. Fortunately, several companies are offering OpenJDK (which is completely free) with commercial licensing. BellSoft Liberica JDK is one of the industry’s leading OpenJDK distributions, 100% Java SE compatible, and supported by an active contributor to this project. Discover Liberica JDK As for the backend, several excellent Java-based enterprise software development frameworks are prominent among the community. Spring from Pivotal, MicroProfile from the Eclipse Foundation, Quarkus from Red Hat, and Micronaut from Object Computing Inc. are the dominant ones in this field. Among them, I will pick Spring since it has been the primary native Java-based enterprise software development framework for the last 20 years and is still going strong in the age of cloud computing. An additional advantage is that Liberica JDK is the default runtime in Spring, which makes our stack of choice uniform and strong. Implementation Developing a full-blown e-commerce app will be too much for a blog post. So we’ll create a simplified store based on microservice architecture. This is going to be a headless backend application without a frontend. However, testing is possible with any REST API client, e.g., by using CURL or POSTMAN. We will create Customer and Order microservices, each of them will handle one specific task according to the general principle of microservice architecture. First, we need to create a Spring Boot project with dependencies using a Spring Initializr. The screenshot below shows how to create an Order microservice by selecting the Spring Boot version, project type Maven, Java 11, and necessary dependencies: Creating an Order microservice via Spring Initializr A Customer microservice is generated in a similar fashion. Configure the Order microservice Domain/Entity The source code is organized in different packages as per the convention in the Domain-Driven Design. The Domain classes are the POJO classes used to shape the API data model. These POJOs are also helpful in persisting the data in MongoDB: its advantage is that we do not need any DTO layer for mapping the REST entities into the database. Below you will find the POJO definitions I have for the Order microservice: POJO for the Order class: @Document(collection = "order") @Getter @Setter @ToString @NoArgsConstructor public class Order implements Serializable { private static final long serialVersionUID = 1L; @Id private String id; @NotBlank @Field("customer_id") private String customerId; @Field("created_at") @CreatedDate private Instant createdAt; @Field("updated_at") @LastModifiedDate private Instant updatedAt; @Version public Integer version; @Field("status") private OrderStatus status = OrderStatus.CREATED; @Field("payment_status") private Boolean paymentStatus = Boolean.FALSE; @NotNull @Field("payment_method") private PaymentType paymentMethod; @NotNull @Field("payment_details") private String paymentDetails; @Field("shipping_address") private Address shippingAddress; @Field("products") @NotEmpty private Set<@Valid Product> products; I’m referring to Project Lombok to reduce the boilerplate code (e.g., Getter, Setter, toString, HashCode, Equals). Moreover, I have used Bean Validation according to the Swagger definitions. Automatic auditing of the orders in the database is possible with the annotations: @CreatedDate, @LastModifiedDate, and @Version. The Order microservice has only one collection (Order), and all its other entities (e.g., products, shippingAddress) can be embedded in the Order collection. Repository I chose MongoDB as a database for my project. There are mainly two ways to access data in MongoDB. One is to use the Java Driver for MongoDB, which supports the native, low-level MongoDB API. Another, simpler option is Spring Data MongoDB. Spring Data is an initiative from Spring to unify and access different kinds of stores (SQL, NoSQL, and others). It applies the repository concept from the Domain-Driven Design. In this demo, I am sticking to Spring Data MongoDB for ease of development. I created the OrderRepository class (interface) to handle the Order entity persistence: @Repository public interface OrderRepository extends MongoRepository { } Controllers Our microservice architecture exposes API so that a client or other microservices could communicate with it using REST. Spring MVC offers a controller pattern to expose REST API with minimum effort. For any application, the best practice is to add a /health endpoint to check whether the application is running. It is also present in Kubernetes to check the liveness of the application by sending “heartbeat” requests. First, create the Health class: @Data @NoArgsConstructor @EqualsAndHashCode @ToString public class Health { private HealthStatus status; } And then — HealthStatus class. HealthStatus enum class: public enum HealthStatus { UP("UP"), DOWN("DOWN"); private final String status; HealthStatus(String status) { this.status = status; } public String getStatus() { return status; } @Override public String toString() { return status; } } Here’s the controller for the /health endpoint: @RestController @RequestMapping("/api/v1") public class HealthResource { private final Logger log = LoggerFactory.getLogger(HealthResource.class); @GetMapping( value = "/health", produces = "application/json") public ResponseEntity getHealth() { log.debug("REST request to get the Health Status"); final var health = new Health(); health.setStatus(HealthStatus.UP); return ResponseEntity.ok().body(health); } } For the Order microservice, the service endpoints are defined in the OrderResource class, offering the REST API for the order-related CRUD operations. For the sake of brevity, we will now create only one method, createOrder (POST request). OrderResource class: @RestController @RequestMapping("/api/v1") public class OrderResource { private final Logger log = LoggerFactory.getLogger(OrderResource.class); private static final String ENTITY_NAME = "order"; @Value("${spring.application.name}") private String applicationName; private final OrderRepository orderRepository; private final OrderService orderService; public OrderResource(OrderRepository orderRepository, OrderService orderService) { this.orderRepository = orderRepository; this.orderService = orderService; } @PostMapping("/orders") @Transactional public ResponseEntity createOrder(@Valid @RequestBody Order order) throws URISyntaxException { log.debug("REST request to save Order : {}", order); if (order.getId() != null) { throw new ResponseStatusException(HttpStatus.CONFLICT, "A new order cannot already have an ID"); } final var result = orderRepository.save(order); orderService.createOrder(result); HttpHeaders headers = new HttpHeaders(); String message = String.format("A new %s is created with identifier %s", ENTITY_NAME, result.getId().toString()); headers.add("X-" + applicationName + "-alert", message); headers.add("X-" + applicationName + "-params", result.getId().toString()); return ResponseEntity.created(new URI("/api/orders/" + result.getId())).headers(headers).body(result); } } The final touch is to add configuration files (application.yml, index.html) and an ApplicationConfiguration class. The ApplicationConfiguration class is quite simple: @Configuration public class ApplicationConfiguration { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } } index.html file in the resources/static directory: Order Microservice

Welcome to the Order Microservice

Current Time:

The order endpoints are here

And finally, two application.yml files, one in the resources directory: spring: application: name: microservice-order microservice-customer: url: https://customer.microservicesdemo.net/customer/api/v1/ data: mongodb: uri: mongodb+srv://mkmongouser:Secret_Password@cluster0.yu4x6.mongodb.net database: order server: port: 8080 servlet: context-path: /order and one in the local directory: spring: application: name: microservice-order microservice-customer: url: http://localhost:8080/customer/api/v1/ devtools: restart: enabled: true data: mongodb: uri: mongodb://localhost:27017 database: order server: port: 8080 servlet: context-path: /order Configure the Customer microservice As for the Customer microservice, it also contains ApplicationConfiguration, Health, HealthStatus, and HealthResource classes identical to those in the Order microservice, so just copy and paste them into your Customer application. Now, let’s define the POJOs for a Customer class. POJOs for the Customer class @Document(collection = "customer") @Data @NoArgsConstructor public class Customer implements Serializable { private static final long serialVersionUID = 1L; @Id private String id; @Field("orders") private Set orders = new HashSet<>(); public Customer addOrder(Order order) { this.orders.add(order); return this; } } POJOs for the Order class: @Getter @Setter @ToString @NoArgsConstructor public class Order implements Serializable { private static final long serialVersionUID = 1L; @Id @NotBlank private String id; @NotBlank private String customerId; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Order order = (Order) o; return id.equals(order.id); } @Override public int hashCode() { return Objects.hash(id); } } Create a CustomerRepository interface similar to the OrderRepository: @Repository public interface CustomerRepository extends MongoRepository { } The service endpoints are defined in the CustomerResource class, offering the REST API for the customer-related CRUD operations. CustomerResource class: @RestController @RequestMapping("/api/v1") public class CustomerResource { private final Logger log = LoggerFactory.getLogger(CustomerResource.class); private static final String ENTITY_NAME = "customer"; @Value("${spring.application.name}") private String applicationName; private final CustomerRepository customerRepository; public CustomerResource(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } @PostMapping("/customers") public ResponseEntity createCustomer(@Valid @RequestBody Customer customer) throws URISyntaxException { log.debug("REST request to save Customer : {}", customer); if (customer.getId() != null) { throw new ResponseStatusException(HttpStatus.CONFLICT, "A new customer cannot already have an ID"); } final var result = customerRepository.save(customer); HttpHeaders headers = new HttpHeaders(); String message = String.format("A new %s is created with identifier %s", ENTITY_NAME, customer.getId()); headers.add("X-" + applicationName + "-alert", message); headers.add("X-" + applicationName + "-params", customer.getId()); return ResponseEntity.created(new URI("/api/customers/" + result.getId())).headers(headers).body(result); } } Here we also have the CustomerOrderResource class offering the REST API to establish a link between the Customer and the Order. CustomerOrderResource class: @RestController @RequestMapping("/api/v1") public class CustomerOrderResource { private final Logger log = LoggerFactory.getLogger(CustomerOrderResource.class); private static final String ENTITY_NAME = "order"; @Value("${spring.application.name}") private String applicationName; private final CustomerRepository customerRepository; public CustomerOrderResource(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } @PostMapping("/customerOrders/{customerId}") public ResponseEntity createOrder(@PathVariable String customerId, @Valid @RequestBody Order order) { log.debug("REST request to save Order : {} for Customer ID: {}", order, customerId); if (customerId.isBlank()) { throw new ResponseStatusException( HttpStatus.NOT_FOUND, "No Customer: " + ENTITY_NAME); } final Optional customerOptional = customerRepository.findById(customerId); if (customerOptional.isPresent()) { final var customer = customerOptional.get(); customer.addOrder(order); customerRepository.save(customer); return ResponseEntity.ok() .body(order); } else { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid Customer: " + ENTITY_NAME); } } } Finally, define the configuration files, index.html and two application.yml files. index.html file: Customer Microservice

Welcome to the Customer Microservice

Current Time:

The customer endpoints are here

The customer-order endpoints are here

application.yml file in the main/local directory: spring: application: name: microservice-customer devtools: restart: enabled: true data: mongodb: uri: mongodb://localhost:27017 database: customer server: port: 8080 servlet: context-path: /customer application.yml file in the resources directory: spring: application: name: microservice-customer data: mongodb: uri: mongodb+srv://mkmongouser:Secret_Password@cluster0.yu4x6.mongodb.net database: customer server: port: 8080 servlet: context-path: /customer Services In Spring projects, the business logic is encapsulated by a service layer. The Order microservice additionally synchronizes with the Customer one and this logic in the OrderService class. OrderService class: @Service @Slf4j public class OrderService { private final Logger log = LoggerFactory.getLogger(OrderService.class); @Autowired RestTemplate restTemplate; @Autowired ObjectMapper objectMapper; @Value("${spring.application.microservice-customer.url}") private String customerBaseUrl; private static final String CUSTOMER_ORDER_URL = "customerOrders/"; public void createOrder(Order order) { final var url = customerBaseUrl + CUSTOMER_ORDER_URL + order.getCustomerId(); final var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); log.info("Order Request URL: {}", url); try { final var request = new HttpEntity<>(order, headers); final var responseEntity = restTemplate.postForEntity(url, request, Order.class); if (responseEntity.getStatusCode().isError()) { log.error("For Order ID: {}, error response: {} is received to create Order in Customer Microservice", order.getId(), responseEntity.getStatusCode().getReasonPhrase()); throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, String.format("For Order UUID: %s, Customer Microservice Message: %s", order.getId(), responseEntity.getStatusCodeValue())); } if (responseEntity.hasBody()) { log.error("Order From Response: {}", responseEntity.getBody().getId()); } } catch (Exception e) { log.error("For Order ID: {}, cannot create Order in Customer Microservice for reason: {}", order.getId(), ExceptionUtils.getRootCauseMessage(e)); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, String.format("For Order UUID: %s, Customer Microservice Response: %d", order.getId(), ExceptionUtils.getRootCauseMessage(e))); } } } Conclusion In this first part of the series, we studied a demo backend Spring Boot application based on microservice architecture. We got the source code with the Controller, Service, Repository, and Entity model. Now the code is compiled and ready for deployment. Both microservices can be enhanced further, with custom exceptions classes, detailed data on customers and orders, etc. Feel free to populate your microservices on your own or head over to my GitHub repository, where I have full-blown Customer and Order microservices with all the additional data. Part two will look into how to prepare and run these microservices in the cloud. Unlocking the bonus: A new Linux tailor-made for Spring apps BellSoft created a lightweight Linux distribution for Cloud and Server use — Alpaquita Linux. Based on Alpine, it boasts small size and a bunch of distinctive features making it a perfect Linux for Java applications: Optimized musl, whose performance is equal or superior to that of glibc Additional glibc-based variant Three additional malloc implementations for various Java workloads Packages with Java tools Perfect compatibility with Liberica JDK Lite and Liberica NIK Support from BellSoft, a major OpenJDK contributor Migration to Alpaquita Containers based on Linerica JDK Lite and Alpaquita is effortless and convenient but brings immediate advantages in terms of reduced memory consumption. Try Alpaquita Containers with your Spring Boot project and tell us what you think! - [Java 19 — new features and enhancements](https://bell-sw.com/announcements/2022/06/22/java-19-new-features-and-enhancements/): JDK 19 acquired a clear form on June 9 when it was moved to Rampdown Phase One. Although examining and playing with new language functionalities is exciting on its own, there is one aspect that makes Java 19 remarkable. It is right in the middle between LTS releases, which means we can already trace the trends of Java evolution. JEPs added to the upcoming version demonstrate that Java strives to be a universal programming language used in any environment, for any task, and with any technology. For instance, the addition of RISC-V port broadens the range of supported platforms, Foreign Function & Memory API enables the usage of data outside of the JVM, and virtual threads will make Java an ideal choice for high-throughput concurrent applications. Thus, we expect this trend to be going strong all the way till the next LTS release and maybe even later. So let’s examine all seven enhancements in-depth and see how they will benefit your application! What autumn will bring to us: Discover the Java 19 JEPs JEP 405: Record Patterns (Preview) JEP 422: Linux/RISC-V Port JEP 424: Foreign Function & Memory API (Preview) JEP 425: Virtual Threads (Preview) JEP 426: Vector API (Fourth Incubator) JEP 427: Pattern matching for switch (Third Preview) JEP 428: Structured Concurrency (Incubator) Conclusion What autumn will bring to us: Discover the Java 19 JEPs JEP 405: Record Patterns (Preview) JEP 405 is a preview feature that improves pattern matching by introducing record patterns to deconstruct record values. Pattern matching promotes more declarative programming in Java. This JEP continues a series of enhancements introduced to JDK 16 (pattern matching for instanceof) and 17 and 18 (pattern matching for switch). Record patterns are nestable, meaning that the record components can be matched against and decomposed by a nested pattern. This enables the developers to write clearer and more concise code avoiding complex object navigation. JEP 422: Linux/RISC-V Port RISC-V is an open-source RISC-based instruction set architecture. These microprocessors have the advantages of RISC architecture (simplicity, flexibility, high performance), and are available for free, meaning that any company or engineer can use them in their development or research. As a result, RISC-V rapidly gets adopted across industries, including embedded systems. Find out more about RISC-V architecture and its benefits in our article dedicated to the topic. JEP 422 ports the JDK to Linux/RISC-V. The port will support the RV64GV configuration of RISC-V, a general-purpose 64-bit ISA with vector instructions, as well as four HotSpot subsystems: The template interpreter The C1 (client) JIT compiler The C2 (server) JIT compiler All current mainline Garbage Collectors, including ZGC and Shenandoah Huawei Technologies, the author of the port, will fully support the code introduced by the JEP through regular updates and tests. The JDK port to RISC-V is an important step toward strengthening the ties between Java and RISC-V. BellSoft believes that both technologies will provide robust solutions in the future. We already have a special version of our OpenJDK distribution, Liberica JDK for Embedded, tailor-made for embedded systems on ARM32 and ARM64. As RISC-V’s potential opens up fully in embedded devices, we plan to add support for this architecture. Meanwhile, Liberica JDK already supports the widest range of platforms and system configurations, which makes it a Unified Java Runtime for any desktop, cloud, or server. Get the white paper on Liberica JDK JEP 424: Foreign Function & Memory API (Preview) This feature was first introduced in JDK 17 as the first incubator and upgraded to the second incubator in JDK 18. In JDK 19, it is already a preview API. The Foreign Function & Memory (FFM) API will replace the Java Native Interface (JNI) and enable the developers to use foreign code and memory not managed by the JVM more efficiently. It will also strengthen the security of such operations as most of the FFM API is safe by design and doesn’t compromise the Java Platform. It will still be possible to call the unsafe methods, but their usage is restricted and comes with a warning at run time. The upgrade summarizes the enhancement made to the feature based on the community’s feedback during two Java releases. JEP 425: Virtual Threads (Preview) This is one of the most exciting features of the upcoming release because its goal is to enhance how the concurrency is handled in Java. The introduction of virtual threads as part of the Project Loom will dramatically improve the performance of high-throughput concurrent applications. Traditional threads in Java programming are wrapped around operating system threads. As a result, they are easy to monitor or debug, but their number is strictly limited. On the other hand, asynchronous programming, which is sometimes used for application scaling, doesn’t allow for efficient thread monitoring. Virtual threads will use the existing java.lang.Thread API but won’t be tied to an OS thread for the whole lifetime, only for the period they perform their calculations. As a result, multiple virtual threads will utilize one OS thread, so the number of tasks performed concurrently can be increased to tens of thousands. At the same time, virtual threads are just as easily profiled and debugged as traditional ones. Find out more in our short article about virtual threads. JEP 426: Vector API (Fourth Incubator) Vector API helps to increase the performance of applications in specific fields such as machine learning, cryptography, finances, etc. The API enables vector computations to compile reliably to vector instructions at runtime. It was first introduced in JDK 16 as an incubation API. JEP 426 is the fourth incubator, which includes the following enhancements based on the developers’ feedback: New functionality to load and store vectors to/from MemorySegments as defined by JEP 424 described above New cross-lane vector operations: compress and expand, as well as vector mask compress operation Additional bitwise integral lane operations counting the number of one bits, leading zero bits, trailing zero bits, reversing the order of bits and bytes, compressing and expanding bits JEP 427: Pattern matching for switch (Third Preview) Pattern matching was introduced in Java 17 as a preview feature and upgraded to the second preview in Java 18. This is a third preview that includes Replacement of guarded patterns with when cases in switch block The alignment of the runtime semantics of the pattern switch with legacy switch semantics in cases when the value of the selector expression is null The goal of this feature is to enrich switch expressions and statements with pattern matching, namely to Allow patterns to appear in case labels Make the pattern switch cover all possible input values, thus increasing safety of operations The existing switch statements will compile without changes. JEP 428: Structured Concurrency (Incubator) Introducing a library for structured concurrency aims to improve multithreaded programming in Java. Multithreaded code implies certain risks such as cancellation delays or memory leaks. This is because tasks performed concurrently run independently, and if one thread throws an exception, other threads do not see that. Structured concurrency enables the coordination of threads in such a way that threads belonging to one task are confined within one syntactic block and react to failures of their siblings. JEP 428 provides valuable support for virtual threads described above. In cases when thousands of virtual threads run concurrently, their proper coordination and structurization are vital for the app’s proper functioning. Read a more detailed yet concise description of this feature in our short article dedicated to structured concurrency. Conclusion To sum up, the enhancements introduced in JDK 19 will help Java gain the upper hand in a broader range of use cases, be it embedded devices, scientific computations, or multithreaded programming. At the same time, Java code becomes more clear and concise slowly leaving the notorious verbosity in the past. Liberica JDK 19 will come out on schedule in September, so soon, you will be able to download it and test the new functionalities with your application. But if you are only considering migrating to OpenJDK, don’t put off and discover our Progressive Java Runtime supported by a major OpenJDK contributor now! Download Liberica JDK - [New JEP draft: Classfile API](https://bell-sw.com/announcements/2022/06/24/new-jep-draft-classfile-api/): Java evolves fast: changes and enhancements are introduced continuously with new releases coming out every six months. But some technologies cannot keep up with the language progression. The new JEP draft: Classfile API is aimed at closing the growing gap between the JVM and the ASM library for working with Java classes. The goal is to replace ASM with an internal library providing a performant and up-to-date API for class file operations. Motivation behind the new feature Although there are a lot of libraries processing bytecode, Java will benefit from its own API for several reasons: A classfile library is usually bundled with an application or a framework. Newer JDK versions may contain class files unknown to the library bundled with an application resulting in runtime errors. It is easier to keep a library belonging to the JDK up-to-date with the JVM specification and JDK versions. New features and projects integrated into Java are associated with complex changes of the JVM, and existing libraries cannot mature so rapidly to support all the new features. It would be better to avoid integrating ASM into the JDK because it contains a lot of legacy code and its architecture doesn’t fit the principles of modern programming. A completely new library will be designed according to the modern software requirements. Description The design of the new API will be based on the following principles: All class file entities will be represented as immutable objects The library shall take the tree structure of class files into consideration The navigation through class file tree should be user-driven, i.e., the library will parse only elements necessary to the user Both streaming and materialized views of the class file should be supported The library should generate the class entities derived from other class file elements based on the methods added to the file Instead of the obsolete Visitor approach, the API will be based on the modern and less verbose Java features such as lambdas, records, and pattern matching There are three key abstractions in the API design: element, builder, and transform. The element is an immutable description of class file elements. The builder has building methods and acts as a Consumer of a specific element type. The transform is a function for element transformation. Conclusion JEP developers have no intention of substituting all existing bytecode libraries. Rather, the new API will be an additional modern tool for writing, parsing, and transforming class files in Java reliably. There is currently a draft of the API at the JEP’s page. The new library will have a large surface area, so multiple quality, performance, and conformance tests will be performed to avoid negative impacts on Java applications. - [How to deploy Docker containers to AWS](https://bell-sw.com/announcements/2022/06/27/how-to-deploy-docker-containers-to-aws/): A guide to building Java microservices. Part 3 Welcome back to our series on building Java microservices! In part one, we created two working microservices, Customer and Order. In part two, we containerized our application using Docker. Now it’s time to send our Docker containers into the cloud! Publish containers Deploy containers in EKS Set up a Database connection Deploy your Docker container Cleanup Conclusion Publish containers To publish our Docker containers to a registry, we’ll use Amazon ECR, a managed container registry to store, share, and deploy containers in the AWS Cloud. First, we should install and configure the AWS Command Line Interface in our local machine using the steps defined in the AWS CLI v2 installation guide. Also, I have configured the CLI with access key ID and secret access key as described in the Configuration and credential file settings from the same source. Now, for each microservice container image, we need to create an ECR repository. Please note the repository name should exactly match the container image repository name. Here’s the command to create a repository in ECR for the Order container image: aws ecr create-repository --repository-name microservice-customer It will create a repository for our Order microservice and will return the following output: { "repository": { "repositoryArn": "arn:aws:ecr:eu-central-1:877546708265:repository/microservice-customer", "registryId": "877546708265", "repositoryName": "microservice-customer", "repositoryUri": "877546708265.dkr.ecr.eu-central-1.amazonaws.com/microservice-customer", "createdAt": "2021-03-04T00:18:33+01:00", "imageTagMutability": "MUTABLE", "imageScanningConfiguration": { "scanOnPush": false }, "encryptionConfiguration": { "encryptionType": "AES256" } } } We need to tag our local Docker image with the ECR registry, repository, and (optional tag) in the next step. For this purpose, we need the Docker Image ID of our local Order microservice container. The next command will give detailed info regarding Docker images: docker image ls microservice-customer:1.0.0 It will return the following output: REPOSITORY TAG IMAGE ID CREATED SIZE microservice-customer 1.0.0 652da8e2130b 41 years ago 274MB Create a tag of our Docker image to the AWS ECR registry and repository: docker tag 652da8e2130b 877546708265.dkr.ecr.eu-central-1.amazonaws.com/microservice-customer:1.0.0 Before publishing the Docker image to ECR, we need to authenticate our Docker there. The authentication will be valid for 12 hours. aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin 877546708265.dkr.ecr.eu-central-1.amazonaws.com You will get a similar message: WARNING! Your password will be stored unencrypted in /home/$USER_NAME/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded Now, you can push the Docker image to AWS ECR with docker push 877546708265.dkr.ecr.eu-central-1.amazonaws.com/microservice-order:1.0.0 Depending on your network speed, it can take up to several minutes. You can now check your pushed image in the ECR repository: Docker image in the ECR repository We can also pull the image from ECR with the following command to test whether the image was correctly uploaded to the repository: docker pull 877546708265.dkr.ecr.eu-central-1.amazonaws.com/microservice-customer:1.0.0 If everything is fine, it will generate the output: 1.0.0: Pulling from microservice-customer Digest: sha256:555b582b3353f9657ee6f28b35923c8d43b5b5d4ab486db896539da51b4f971a Status: Image is up to date for 877546708265.dkr.ecr.eu-central-1.amazonaws.com/microservice-customer:1.0.0 877546708265.dkr.ecr.eu-central-1.amazonaws.com/microservice-customer:1.0.0 Deploy containers in EKS Set up a Database connection For your microservices to connect with the database, you need to create a MongoDB cluster. Sign up at MongoDB Atlas. After the registration, create a free Shared cluster, choose cloud provider (AWS) and region, press the “Create Cluster” button. After that, create a user with a username and password. Once your cluster gets generated, you will see the following: MongoDB Cluster Now, you need to generate the connection string for the Atlas cluster so that our Spring Boot microservices can connect with it. Press “Connect”, select “Connect your application”. Please note that the connection string is dependent on the programming language and your MongoDB version. Follow the instructions on the screen to establish your connection. Deploy your Docker container Kubernetes is the de-facto container orchestration infrastructure. It is the open-source system initially developed by Google but now backed by the whole industry. It facilitates the deployment, scaling, and management of containerized applications. Kubernetes works perfectly with Docker Desktop, where it is included as a standalone server and client. Kubernetes still needs operational efforts, and Managed Kubernetes is a better approach to focus on code entirely. For our cloud-native development use case, I will turn to Amazon Elastic Kubernetes Service (EKS) that enables the developers to start, run, and scale Kubernetes applications in AWS Cloud or on-prem. We need to install the eksctl command-line utility to manage the EKS cluster and the Kubernetes command-line tool kubectl. Now, we can create an EKS cluster using the eksctl command: eksctl create cluster \ --name microservices \ --region eu-central-1 \ --node-type t2.small \ --nodes 2 It will make a cluster with two worker nodes of type “t2.small” in the region “eu-central-1” with the name “microservice.” In the background, eksctl uses CloudFormation to create the cluster, which usually takes 10–15 minutes. After the cluster creation is complete, you’ll get the following output: [✔] saved kubeconfig as "/home//.kube/config" [ℹ] no tasks [✔] all EKS cluster resources for "microservices" have been created [ℹ] adding identity "arn:aws:iam::877546708265:role/eksctl-microservices-nodegroup-ng-NodeInstanceRole-9PQCLZR7NSYS" to auth ConfigMap [ℹ] nodegroup "ng-3e8fb16c" has 0 node(s) [ℹ] waiting for at least 2 node(s) to become ready in "ng-3e8fb16c" [ℹ] nodegroup "ng-3e8fb16c" has 2 node(s) [ℹ] node "ip-192-168-77-195.eu-central-1.compute.internal" is ready [ℹ] node "ip-192-168-9-13.eu-central-1.compute.internal" is ready [ℹ] kubectl command should work with "/home//.kube/config", try 'kubectl get nodes' [✔] EKS cluster "microservices" in "eu-central-1" region is ready From the output, it is evident that it has created two nodes and one node group. Also, it has saved the kubectl config file in ./.kube/config. In case you already have minikube or microk8s, you have to mention the ./.kube/config file as the kubeconfig parameter in the command. Creating an EKS cluster will take around 15 minutes. Once the cluster is ready, you can check it by running kubectl get nodes --kubeconfig ~/.kube/config It will return as follows: NAME STATUS ROLES AGE VERSION ip-192-168-77-195.eu-central-1.compute.internal Ready 10m v1.18.9-eks-d1db3c ip-192-168-9-13.eu-central-1.compute.internal Ready 10m v1.18.9-eks-d1db3c Let’s move on. Define the Kubernetes deployment file to deploy the application: apiVersion: apps/v1 kind: Deployment metadata: name: microservice-deployment labels: app: microservice-customer spec: replicas: 1 selector: matchLabels: app: microservice-customer template: metadata: labels: app: microservice-customer spec: containers: - name: microservice-customer-container image: 877546708265.dkr.ecr.eu-central-1.amazonaws.com/microservice-customer:1.0.0 ports: - containerPort: 8080 Here we have defined the Kubernetes deployment file as well as a load balancer. We are now ready to deploy our application in Kubernetes. Run kubectl apply -f eks-deployment.yaml --kubeconfig ~/.kube/config You will have the response: deployment.apps/microservice-deployment created Check the status of the pods: --kubeconfig ~/.kube/config This command will show the following output with the pod status as running: NAME READY STATUS RESTARTS AGE microservice-deployment-597bd7749b-wcfsz 1/1 Running 0 13m We can also check the log file of the pod with kubectl logs microservice-deployment-597bd7749b-wcfsz --kubeconfig ~/.kube/config It should show a log message like this one: 2021-03-04 00:09:50.848 INFO 1 --- [ngodb.net:27017] org.mongodb.driver.cluster : Discovered replica set primary clustermicroservice-shard-00-01.fzatn.mongodb.net:27017 2021-03-04 00:09:52.778 INFO 1 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2021-03-04 00:09:53.088 INFO 1 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html] 2021-03-04 00:09:53.547 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '/customer' 2021-03-04 00:09:54.602 INFO 1 --- [ main] o.m.m.customer.CustomerApplication From this, we can see that the Customer microservice can successfully connect with MongoDB Atlas, started on port 8080. Although our Customer microservice is deployed correctly in the EKS cluster, it is still not reachable from outside. We need to create a Kubernetes Service Controller, which will expose an external IP address and make our deployed pods available from outside. Here is the definition of the Kubernetes service: apiVersion: v1 kind: Service metadata: name: microservice-customer-service spec: #Creating a service of type load balancer. Load balancer gets created but takes time to reflect type: LoadBalancer selector: app: microservice-customer ports: - protocol: TCP port: 80 targetPort: 8080 Please note that the targetPort should be the same as the containerPort defined in the deployment description (in our case 8080). We can deploy our Service into AWS with the following command: kubectl apply -f eks-service.yaml --kubeconfig ~/.kube/config It will return as follows: service/microservice-customer-service created The Kubernetes service will be mapped in Elastic Load Balancer (ELB) of AWS. Now, we can check the external IP address of the service. Run kubectl get svc --kubeconfig ~/.kube/config It should return the following: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE Kubernetes ClusterIP 10.100.0.1 443/TCP 39m microservice-customer-service LoadBalancer 10.100.94.75 aa62f80b9596a4fa6835d80a506227d6-1183908486.eu-central-1.elb.amazonaws.com 80:32248/TCP 21s From the above response, we can see that the load balancer is available with the external IP address: aa62f80b9596a4fa6835d80a506227d6-1183908486.eu-central-1.elb.amazonaws.com Moreover, we can check the ELB of AWS: checking the ELB of AWS Please note that the DNS name in the ELB is the same as the external IP address of the service previously received. Opening a browser at the external IP address of the service at this point will give you this: Using the external IP address of the service Similarly, we can deploy our Order microservice container in the EKS cluster by repeating the above mentioned steps: Create Docker Image, Publish Docker Image to ECR, Deploy Docker Image in EKS. For that, first, we need to put the ELB endpoint of the Customer microservice in the application.yml file of the Order microservice: spring: application: name: microservice-order microservice-customer: url: http://aa62f80b9596a4fa6835d80a506227d6-1183908486.eu-central-1.elb.amazonaws.com/customer/api/v1/ data: mongodb: uri: mongodb+srv://mkmongouser:@clustermicroservice.fzatn.mongodb.net database: order server: port: 8080 servlet: context-path: /order Otherwise, the steps are identical to the Customer microservice. Cleanup Running the AWS ECS cluster will incur costs, including the costs of the full EKS infrastructure (master node), worker nodes, load balancers, and a node group. In a production environment, we will let them run 24/7. But in our testing case, it is better to clean the resources created by the EKS Cluster. To clean up the resources, run eksctl delete cluster --name microservices Conclusion The application is complete! We have deployed our Docker container in Kubernetes and EKS. Again, if you’d like to see the complete project, head over to my GitHub: there, you will find the repos for both the Customer and Order microservices. Next time we’ll deal with everything related to monitoring. Stay tuned for valuable advice about JFR streaming in the cloud. We’ll look at this simple app’s performance and learn how to handle failure incidents. - [Enabling security and monitoring of cloud-native microservices](https://bell-sw.com/announcements/2022/06/28/enabling-security-and-monitoring-of-cloud-native-microservices/): A guide to building Java microservices. Part 4 Custom Domain for our Applications Enable Security (HTTPS) Logging with Fluent Bit Monitoring with CloudWatch Container Insights Summary This is the fourth part of the cloud-native microservice development in Java™. In the first part of the series, we designed the microservice architecture and developed two simple Java microservices. In the second part, we containerized our applications using the cloud-native buildpack implementation paketo.io., which is natively supported by Spring Boot. We created a container image of our microservices using Liberica JDK. In the third part, the container image was first published in the AWS Container Registry ECR and later deployed on the managed Kubernetes service of Amazon Cloud (EKS). However, there are still many things to do to make our microservices ready for enterprise use. The complete code for our Customer and Order microservices is available on GitHub. Custom Domain for our Applications Amazon Route 53 is a highly-available and scalable Cloud DNS service. In addition to the classic DNS routing, it also offers additional domain registration and health checking. Even though we can use another DNS provider, we will be registering our customer domain in Amazon Route 53, which is a straightforward procedure. First, open “Route 53” and select “Register domain” as shown below: Route 53 Dashboard I have chosen the domain name “microservicesdemo”. After that, a number of available domain names with “microservicesdemo” is displayed. I went for microservicesdemo.net, which costs 11 USD per year. Choosing domain name On the next page, contact details for the domain registration are shown. Once the contact details are filled, the domain is created. Contact details for the domain After the registration of the domain name, Route 53 is automatically set as DNS service for the registered domain. Route 53 also creates a “Hosted zone” with the same name as the registered domain. We want to use two subdomains for our two microservices and also route traffic from the sub-domains to load balancers. For the latter purpose, we need to create a “Record” in the “Hosted zone” of Route 53. Creating a record In the ”Routing policy”, we choose “Simple routing”. In “Configure record”, we define a “simple record” to configure a subdomain for our Customer microservice so that the traffic from our subdomain will be routed to the customer load balancer. Defining a simple record Now, we can reach our Customer microservice with the subdomain name: Customer Microservice The Route 53 DNS Service routes the traffic from the subdomain to the customer load balancer. We can repeat the above steps to configure the Order microservice subdomain as well. Enable Security (HTTPS) For any kind of enterprise application, securing the web traffic via TLS 1.2+ is a must-have criteria. The entire traffic between the client browser and the load balancer is TLS terminated (HTTPS encrypted/decrypted). Please note that the connection between the load balancer and the pods is not TLS encrypted as shown below: SSL/TLS Termination We will use the Amazon Certificate Manager to provide, manage, and deploy SSL/TLS certificates, which we will use in our load balancers. Let’s open the Amazon Certificate Manager to provide a public certificate: AWS Certificate Manager Now, we have to request a certificate from ACM: Requesting a certificate On the next page, we provide our domain name which we previously configured. Providing a domain name AWS needs to make sure that we have control over our configured domain. We will use the “DNS validation” method as we have full control of our domain name: DNS validation Once we place our requisition to provide the certificate, it shows the CNAME record, which needs to be added to the DNS configuration of the domain. We can choose the option “Create record in Route 53” which will automatically add a “Record” with the CNAME in the “Hosted zone” configuration of the Route 53: Create record in Route 53 Once the certificate is provided, it will show the following: Certificate Please note that it shows the status as “issued” for our certificate indicating that our certificate is correctly issued by AWS. It shows “in use” as “no” because we have not configured the certificate yet. Now, we need to terminate the SSL/TLS in the load balancer. Please note that we will not encrypt the connection between the load balancer and the pods. We will create a new Load Balancer Service “eks-service-tls.yaml” in the directory “src/main/k8s”: apiVersion: v1 kind: Service metadata: name: microservice-customer-service annotations: service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:eu-central-1:877546708265:certificate/113c4264-84bd-46e0-906f-a7b1bf1e0626 service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http spec: #Creating a service of type load balancer. Load balancer gets created but takes time to reflect type: LoadBalancer selector: app: microservice-customer ports: - protocol: TCP port: 443 targetPort: 8080 There are several differences compared to our Load Balancer Service definition we last used. In the annotations, we configured the certificates which we previously created in ACM: annotations: service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:eu-central-1:877546708265:certificate/113c4264-84bd-46e0-906f-a7b1bf1e0626 service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http In addition, we defined the backend protocol as http. We wanted to have the “X-Forwarded-Proto” headers in the backend service, so we configured the “service.beta.kubernetes.io/aws-load-balancer-backend-protocol” as http. Also, the port is changed to “443” for https. Now, we can reach our Customer microservice at the address: https://customer.microservicesdemo.net/customer Viewing the certificate We can see the lock button near the URL indicating that our connection is secure. Expansion of the lock button will provide the detailed information about the SSL/TLS certificate (e.g. issued by Amazon, issue date, etc). We can repeat the whole process to secure the connection with Order microservice as well. Logging with Fluent Bit Logging is the integral part of software development. It must be supported by all production applications. We have to log all the important events during the application lifecycle for data analysis. Here are the main use cases of logging: If there is an error in the application, we can analyze the log data to find the root cause. If there is a bug in the application, we can reproduce the bug by looking at the log data. Understanding the whole workflow of the application. In our open-source e-commerce application, we generate log data. The challenge is to collect and process them. There are many ways to export log data from applications. We can write to files, log collector API, or the STDOUT (console) and use a log collector for data collection. According to the twelve factor apps, which is a gold standard of modern application development, an application should not write to logfiles or log collectors. Instead, an application writes its logs to the unbuffered STDOUT as an event stream. During local development, the developers should be able to view the log events in the foreground of their console. In staging or production, the logs are captured by the execution environment. The log routers/collectors collect the logs and route them to the final destination (file or database) for storage and analysis. In our e-commerce app, we will use the approach suggested by the “12 factor apps logging”: logs will be generated as event stream and captured by the Kubernetes pods, a log collector and analyser will then collect the logs, and we will use Amazon Cloud Watch as the final destination of our log data for analysis. There are many log collectors in the market. Among them, Fluentd is most widely used for its vendor neutral features. In recent years, it has gained popularity due its lightweight yet powerful features. Moreover, Fluent Bit has a particular focus on collecting logs in the containerized environment. In our application, we will use Fluent Bit as a log collector to collect log data from our pods. We will use Amazon CloudWatch Container Insights for log analysis. All the components for logging are shown below: Logging with FluentBit and CloudWatch Fluent Bit is an open source Log Processor and Forwarder which allows us to collect various data such as metrics and logs from different sources, enrich them with filters, and send them to multiple destinations including CloudWatch logs. It’s the preferred choice for containerized environments, e.g. Kubernetes. Fluent Bit is designed with performance in mind. It is written in C to effectively solve the narrow log collection problem at scale, and it offers high throughput with low CPU and memory usage. In our EKS cluster, we have the following kinds of logs: Application logs are produced by our application and stored in /var/log/containers/*.log Host logs are system logs generated by the EKS Host nodes and stored in /var/log/messages,/var/log/dmesg,/var/log/secure files Data Plane logs are generated by the EKS Data Plane components. Kubernetes has the concept of DaemonSet to make sure that all nodes run a copy of the pod. It is useful for the Kubernetes cluster wise-operations such as log collection, node monitoring, where pods are automatically added to a new node. DaemonSet is a great point to make an assertion about slim docker images. DaemonSet is in always-restart mode (tries to make a pull, should hit the cache though). And there are easy scale-in, scale-out, and platform updates when pulls are real. Smaller images can help you in this regard, and both Alpine LInux and Liberica JDK Lite (combined in images like bellsoft/liberica-openjdk-alpine-musl) reduce image size dramatically (base image <100 mb on disk). We will set up Fluent Bit as DaemonSet to send logs to the AWS CloudWatch logs. We need to grant IAM permissions to the Kubernetes worker node so that metrics and logs are sent to CloudWatch. This is accomplished by attaching a policy to the IAM roles of the worker nodes. Please note that it is a cluster administration task and it is done once during the cluster setup; it is not a daily task. First, the IAM roles of the worker nodes (EC2 instances) are selected: IAM Role Then the policy “CloudWatchAgentServerPolicy” is attached to the IAM roles: CloudWatch Agent Server Policy Create a namespace “amazon-cloudwatch” with the following command: kubectl apply -f https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/cloudwatch-namespace.yaml --kubeconfig ~/.kube/config Create a ConfigMap named “fluent-bit-cluster-info” with the following command: ClusterName=microservices RegionName=eu-central-1 FluentBitHttpPort='2020' FluentBitReadFromHead='Off' [[ ${FluentBitReadFromHead} = 'On' ]] && FluentBitReadFromTail='Off'|| FluentBitReadFromTail='On' [[ -z ${FluentBitHttpPort} ]] && FluentBitHttpServer='Off' || FluentBitHttpServer='On' kubectl create configmap fluent-bit-cluster-info \ --from-literal=cluster.name=${ClusterName} \ --from-literal=http.server=${FluentBitHttpServer} \ --from-literal=http.port=${FluentBitHttpPort} \ --from-literal=read.head=${FluentBitReadFromHead} \ --from-literal=read.tail=${FluentBitReadFromTail} \ --from-literal=logs.region=${RegionName} -n amazon-cloudwatch \ --kubeconfig ~/.kube/config Here, FluentBitHttpServer is set by default. In addition, Fluent Bit reads log files from the tail, and will capture only new logs after it is deployed. Download and deploy the Fluent Bit DaemonSet using the following command: kubectl apply -f https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/fluent-bit/fluent-bit.yaml --kubeconfig ~/.kube/config The above steps create the following resources in the cluster: A service account named Fluent-Bit in the amazon-cloudwatch namespace. It is used to run the Fluent Bit daemonSet. A cluster role named Fluent-Bit-role in the amazon-cloudwatch namespace. It grants get, list, and watch permissions on pod logs to the Fluent-Bit service account. A ConfigMap named Fluent-Bit-config in the amazon-cloudwatch namespace. It contains the configuration to be used by Fluent Bit. Validate the deployment using the following command: kubectl get pods -n amazon-cloudwatch --kubeconfig ~/.kube/config It should show a pod starting with “fluent-bit-*” for each node. For our EKS cluster, the following response is returned: NAME READY STATUS RESTARTS AGE fluent-bit-8xdlg 1/1 Running 0 11m fluent-bit-rmbw6 1/1 Running 0 11m In the AWS console, the fluent-bit DaemonSet is shown in the EKS cluster: fluent-bit DaemonSet Now, we can check if the Fluent Bit is correctly configured by going to the CloudWatch console. Please make sure that the region in the CloudWatch Console matches the region of the EKS cluster (which is eu-central-1). In the log, three log groups are available as shown below: Log groups We can inspect the /aws/containerinsights/microservices/application log group, which contains all the log events of the microservices. We can filter the log events and have a look at the aggregated log. For our e-commerce app, creating an order with invalid customer ID returns “500 Internal Server Error”. We can easily see the error logs for this event as shown below: Error logs Monitoring with CloudWatch Container Insights Monitoring cloud-native microservices is a must for production and deployment. There are many tools to monitor Kubernetes deployment. Amazon CloudWatch is a monitoring service to monitor EC2 clusters. Amazon CloudWatch also offers Container Insights to monitor, troubleshoot, and set alarms for AWS Elastic Kubernetes Service (EKS) and AWS Elastic Container Service (ECS). The CloudWatch Container Insights dashboard gives access to the following information: CPU and memory utilization Task and service counts Read/write storage Network Rx/Tx Container instance counts for clusters, services, and tasks To enable CloudWatch Container Insights, we need to deploy a CloudWatch agent with FluentBit in our EKS cluster. The following command will deploy a CloudWatch agent with FluentBit: ClusterName=microservices RegionName=eu-central-1 FluentBitHttpPort='2020' FluentBitReadFromHead='Off' [[ ${FluentBitReadFromHead} = 'On' ]] && FluentBitReadFromTail='Off'|| FluentBitReadFromTail='On' [[ -z ${FluentBitHttpPort} ]] && FluentBitHttpServer='Off' || FluentBitHttpServer='On' curl https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed 's//'${ClusterName}'/;s//'${RegionName}'/;s//"'${FluentBitHttpServer}'"/;s//"'${FluentBitHttpPort}'"/;s//"'${FluentBitReadFromHead}'"/;s//"'${FluentBitReadFromTail}'"/' | kubectl apply -f - --kubeconfig ~/.kube/config We can now visit the CloudWatch Container Insights dashboard for performance monitoring of our EKS cluster: CloudWatch Container Insights Summary As you can see, Java is the perfect programming language for building and maintaining cloud-native microservices. In this article, the code of our microservices became a real production application with a public domain, monitoring, and output log collection. We utilized the AWS Services Route 53 for public domain configuration, AWS Certificate Manager for managing the certificate for SSL/TLS connection, FluentBit for log collection and CloudWatch Container Insights as our Java monitoring tool. Furthermore, there is an administrative section to assign the IAM roles for CloudWatch agent configuration, which is a non-repetitive task. With the CloudWatch Container Insights monitoring, we can monitor and set alarms for application events, server events, and so on. However, tracing and fine JVM monitoring is not covered by logging and monitoring. For distributed tracing, we need to support tracing tools, such as Zipkin or Jaeger. For JVM monitoring, we need to use a tool, such as Prometheus, which supports JMX. And as Java is the perfect fintech programming language, Liberica JDK is the perfect Java Development Kit. Try it for free and see for yourself! - [HotSpot vs. OpenJ9: performance comparison](https://bell-sw.com/announcements/2022/06/28/hotspot-vs-openj9-performance-comparison/): Which Java Virtual Machine to choose, HotSpot or OpenJ9? Both are tunable open-source JVM implementations. HotSpot is a well-established JVM implementation initially developed by Sun Microsystems. OpenJ9, developed by IBM, is not as widespread in the industry but has gained popularity in recent years. OpenJ9 claims great performance in terms of startup time, latency, throughput, and memory footprint, based on the DayTrader7 benchmark application study, where three fine-tuned OpenJ9 configurations are compared to default HotSpot. BellSoft’s engineers have decided to check whether HotSpot can be configured so that it displays comparable or better performance results. You can find the summary of their testing below. Did you know hat there's a great way to reduce Java application startup and warmup from minutes to milliseconds? Try using Coordinated Restore at Checkpoint API. Read more about Java with CRaC support to find out how this functionality will benefit your cloud deployments. BellSoft also provides ready-to-use Alpaquita Containers for "plug-and-play" experience with the feature: follow our guide on using CRaC with Java applications in a container. Table of Contents Experiment setup Results Startup Footprint Latency and throughput Conclusion Afterword: more performance tips Experiment setup We used the DayTrader7 application from the original study as a benchmark. It is not a microservice application but rather a small monolith launched on a web server, so we consider this configuration the most remarkable. We also utilized a server-class machine on Linux and a desktop-class machine on Windows as platforms and OpenJDK 11 as JDK binaries: AdoptOpenJDK build with three OpenJ9 flag configurations and Liberica JDK with default and tuned HotSpot. The focus was on the startup time, footprint, latency, and throughput. Apache JMeter 5.4.1 was utilized to measure three latter metrics. We had no goal of generating synthetic performance numbers with HotSpot fine-tuning tricks but rather evaluating the standard settings of this JVM implementation. In case you want to know what makes Liberica JDK — one of the runtimes utilized in this experiment — different from other OpenJDK distributions and Oracle Java, visit the product page or download the white paper with the full information on its capabilities and support options. Download white paper on Liberica JDK Results Startup There are several parameters that help reduce the application startup with HotSpot. The first one is Application Class Data Sharing or AppCDS. It allows placing application classes in the shared archive, thus accelerating startup. This feature appeared in HotSpot 1.5 and has been improved in subsequent versions. For example, now it is possible to generate archives automatically. The following flag is needed to activate AppCDS: -XX:SharedArchiveFile=app-cds.jsa Note that you must create the AppCDS archive explicitly. The parameter above enables you to specify the name of the archive file for class storage. Without it, the classes will be stored in the JDK installation directory, which is undesirable. After creating the archive, you can launch the application with it, and the JVM maps it into its memory and has most classes it requires available. The AppCDS documentation can be found on Oracle’s website. In addition, the -XX:TieredStopAtLevel=1 parameter enables the developers to run the application with the client (C1) compiler without profiling, which provides faster code compilation. This configuration is suitable in case startup acceleration is a top priority. Below you will find the startup time comparison with OpenJ9 and HotSpot. The startup time was calculated as the difference between the startup time and “Application daytrader7 started in X seconds“ output. Tested configurations: OpenJ9 (1) (heap 256 MB): “-Xmx256m“ OpenJ9 (2) (heap 256 MB, class cache): “-Xmx256m -Xshareclasses:name=mvn” OpenJ9 (3) (heap 256 MB, class cache, fast compilation): “-Xmx256m -Xshareclasses:name=mvn -Xtune:virtualized -Xscmx200m” HotSpot (heap 256 MB): “-Xmx256m” HotSpot (heap 256 MB, AppCDS, C1): “-Xmx256m -XX:SharedArchiveFile=app-cds.jsa -XX:TieredStopAtLevel=1” Startup time (ms) OpenJ9 (1) (heap 256 MB) OpenJ9 (2) (heap 256 MB, class cache) OpenJ9 (3) (heap 256 MB, class cache, fast compilation) HotSpot (heap 256 MB) HotSpot (heap 256 MB, AppCDS, C1) 6.986 s 6.064 s 6.159 s 6.366 s 4.972 s Startup time results (lower is better) Liberica JDK with aforementioned parameters demonstrated the best result (4.972 ms), whereas the OpenJ9 without additional parameters apart from heap size limitation gave the worst outcome (6.986 ms). Footprint For our experiment, we used the daytrader7.jmx test plan offered by Daytrader7 developers. Test plan parameters were adjusted according to the machine class: four threads were used for the server-class machine, and six — for the desktop-class one. Test plan is a script including many HTTP requests imitating the user’s work in the online stock trading system. A user can log in / log out, look up a portfolio or stock quotes, and buy or sell stock shares. Before measuring statistics, one virtual machine warm-up was performed. After that, 5–7 iterations on average were made, 120–300 s each with 30 s breaks. Finally, the mean was calculated. Before measuring each configuration (warm-up run), a daytrader database was created and filled with 15 thousands users. The database was reset prior to each iteration within configuration measurement. The default HotSpot consumes more memory than OpenJ9 when tested under load. However, the same -XX:SharedArchiveFile=app-cds.jsa and -XX:TieredStopAtLevel=1 parameters used for decreasing startup time provided the 30% footprint reduction. This is due to the fact that the C1 compiler uses less memory in the code cache because there are no additional intermediate expressions of the compiled code. And AppCDS minimizes footprint thanks to the common class metadata shared by different JVMs. Tested configurations: OpenJ9 (1) (heap 1G): “-Xmx1G“ OpenJ9 (2) (heap 1G, class cache): “-Xmx1G -Xshareclasses:name=mvn” OpenJ9 (3) (heap 1G, class cache, fast compilation): “-Xmx1G -Xshareclasses:name=mvn -Xtune:virtualized -Xscmx200m” HotSpot (heap 1G): “-Xmx1G” HotSpot (heap 1G, AppCDS, C1): “-Xmx1G -XX:SharedArchiveFile=app-cds.jsa -XX:TieredStopAtLevel=1” Memory consumption (at ~126 s) OpenJ9 (1) (heap 1G) OpenJ9 (2) (heap 1G, class cache) OpenJ9 (3) (heap 1G, class cache, fast compilation) HotSpot (heap 1G) HotSpot with (heap 1G, AppCDS, C1) 418.6 MB 423.7 MB 389.6 MB 612.8 MB 424.7 MB Footprint results (lower is better) The test plan included a combination of various queries executing complex operations. Red dots on the graph mark the startup time. The maximum heap size was set to 256 MB (the -Xmx256m flag). The indicators were measured at ~126 s. This point was chosen because different HotSpot and OpenJ9 configurations have different startup times. After the application startup, memory consumption increases due to the load and then stabilizes. As you can notice on the graph, OpenJ9 demonstrates leaps in memory consumption prior to stabilization. This memory is required for the warm-up, and it may become a problem because the application consumes more memory than we expect in stable condition. So if we set the memory limits to what the app requires after the warm-up, stabilization time may increase, and peak performance falls. Memory consumption prior to stabilization As far as the leaps are concerned, they deserve separate research of optimization strategy. For example, Liberica Lite, which is a lightweight version of our Liberica JDK, can release memory for workflows not under load, and OpenJ9 has recently received JIT as a Service, which enables the VM to carry the peak memory consumption from many instances over to one compilation service. In addition, Liberica Lite enables the creation of microcontainers for a drastic reduction of resource consumption. This metric is especially valuable for cloud computing, where small containers equal cost-efficiency. A container based on Liberica Lite and Alpaquita Linux is only 42MB — the tiniest on the market so far! Returning to our study, our goal was to evaluate the app memory consumption in a stabilized condition. After 126 s, all VM configurations demonstrated stable values, which could be measured. It can be seen on the graph that the configured HotSpot is equal to or superior to two OpenJ9 configurations. Latency and throughput The maximum heap size was set to 1 GB in this set of experiments (-Xmx1G). This way, we could analyze the app behavior within the limits of a typical small cloud node. The default HotSpot configuration demonstrates the best throughput (5% higher than the best OpenJ9 configuration) and latency (69% lower than the best OpenJ9 configuration) but falls behind OpenJ9 in terms of memory consumption. The -XX:SharedArchiveFile=app-cds.jsa -XX:TieredStopAtLevel=1 flags optimized the footprint indicators but didn’t affect throughput and latency. However, HotSpot has a wide selection of Garbage Collectors, and we can use the one most suitable for our case. Here, we switched to Serial Garbage Collector (the -XX:+UseSerialGC parameter), providing less overhead. Serial GC is the simplest garbage collector implementation, which works with one thread, thus reducing thread overhead. It is most suitable for single-processor machines or applications with small heaps. In addition, to decrease the footprint, we set InitialHeapSize at 80MB (the -Xms80m flag). Tested configurations: OpenJ9 (1) (heap 1G): “-Xmx1G“ OpenJ9 (2) (heap 1G, class cache): “-Xmx1G -Xshareclasses:name=mvn” OpenJ9 (3) (heap 1G, class cache, fast compilation): “-Xmx1G -Xshareclasses:name=mvn -Xtune:virtualized -Xscmx200m” HotSpot (max heap 1G, initial heap 80 MB): “-Xmx1G -Xms80m” HotSpot (max heap 1G, initial heap 80 MB, AppCDS, C1): “-Xmx1G -Xms80m -XX:SharedArchiveFile=app-cds.jsa -XX:TieredStopAtLevel=1” HotSpot (max heap 1G, initial heap 80 MB, SerialGC): “-Xmx1G -Xms80m -XX:SharedArchiveFile=app-cds.jsa -XX:TieredStopAtLevel=1 -XX:+UseSerialGC“ Latency (~99.998%) OpenJ9 (1) (heap 1G) OpenJ9 (2) (heap 1G, class cache) OpenJ9 (3) (heap 1G, class cache, fast compilation) HotSpot (max heap 1G, initial heap 80 MB) HotSpot (max heap 1G, initial heap 80 MB, SerialGC) HotSpot (max heap 1G, initial heap 80 MB, AppCDS, C1) 2091 ms 386 ms 641 ms 120 ms 123 ms 1852 ms Latency by percentile distribution (lower is better) The indicators were measured at ~99.998% percentile. As can be seen on the graph, the default HotSpot configuration had one of the best indicators and made it to TOP-3 (lib-def-xms80m), but the results with -XX:SharedArchiveFile=app-cds.jsa -XX:TieredStopAtLevel=1 parameters are the opposite (lib-xms80m). Still, even this configuration is associated with lower latency than OpenJ9 (1), which shows the worst outcomes. However, HotSpot with SerialGC (lib-xms80m-serialgc) demonstrates the best results in terms of latency and memory. Throughput OpenJ9 (1) (heap 1G) OpenJ9 (2) (heap 1G, class cache) OpenJ9 (3) (heap 1G, class cache, fast compilation) HotSpot (max heap 1G, initial heap 80 MB) HotSpot (max heap 1G, initial heap 80 MB, SerialGC) HotSpot (max heap 1G, initial heap 80 MB, AppCDS, C1) 854.1 tps 779.1 tps 758.5 tps 897.9 tps 592.9 tps 594.1 tps Throughput results (higher is better) The graph above shows that the default HotSpot with G1 has the best throughput but leaves a more significant footprint. You can choose the configuration with Serial GC or AppCDS to focus on better latency or memory footprint indicators. You can also use parallel GC, which uses multiple threads for garbage collector acceleration and helps to improve throughput. Conclusion The experiment results are similar on all tested platforms and demonstrate that Liberica JDK with configured HotSpot is comparable to or better than OpenJ9. It is worth noting that the default HotSpot gives much lower latency and higher throughput than OpenJ9 on a server-class node, which is beneficial for long-term cost reduction. Afterword: more performance tips If you took an interest in this topic, you likely care about your apps’ performance. There are multiple ways you can further enhance your development and deployment environment in addition to choosing and setting up JVM: A significant memory footprint left by HotSpot can be minimized by switching to another VM image (Alpine Linux or Alpaquita Linux use twice as less RAM as CentOS). Compare speed and features of different operating systems and choose the one that works better for you. Make sure you choose the correct garbage collector and set it up properly. Analyze your apps’ execution with perf or similar tools to find the issues that affect the performance. You can introduce native image technology into the project. It will accelerate the startup and lower the memory consumption. To help with that, BellSoft provides Liberica Native Image Kit, a utility used by default in Cloud Native Buildpacks, which Spring utilizes to convert Spring Boot applications into native images. You can find more guides and recommendations on monitoring and optimizing Java code performance with JDK Flight Recorder and Mission Control in Liberica JDK Documentation. And there's one more thing you can do with HotSpot as opposed to OpenJ9. If your project is based on JDK 11 and you are not planning to migrate to a newer Java version soon, you can bring the power of JVM 17 to your application with Liberica JDK Performance Edition, which couples JVM 17 and JDK 11. Enjoy instant performance boost with little to no code adjustments and migrate at your own pace! - [Linux distributions for server and cloud: overview](https://bell-sw.com/announcements/2022/06/29/linux-distributions-for-server-and-cloud-overview/): Choosing an optimal solution for your business Windows might be the most popular OS for desktop use, but Linux has the upper hand in the world of server applications and cloud computing. However, it is challenging to choose a perfect Linux distribution for server or cloud with the abundance of distros available on the market. Bottom line is that it should be small to help you save resources and money, and have reliable support that provides you with timely updates and prompt bug fixes. We decided to help you with the choice and compared the most popular Linux server/cloud distros based on their size and what they offer. The summary with key characteristics, pros, and cons of each Linux is presented below. Table of Contents Choosing an optimal solution for your business Best Linux server/cloud distros 1. Alpine Linux 2. Alpaquita Linux 3. Ubuntu 4. Debian 5. RHEL 6. CentOS 7. Amazon Linux 2 8. Oracle Linux Comparative table for Linux server/cloud distributions Best Linux server/cloud distros 1. Alpine Linux Alpine Linux Benefits: Lightweight Drawbacks: Decreased performance due to musl No commercial support Base container image size (compressed): 2.67MB (alpine:3.16) Alpine Linux is the smallest Linux distribution in this overview. The base image size of only 2.67MB enables the developers to create microcontainers for Java applications and thus reduce cloud costs significantly. Alpine uses musl and BusyBox instead of glibc and GNU Core utilities and doesn’t contain any unnecessary packages, so it is as tiny and straightforward as Linux can be. It is also 100% free and community-based. Furthermore, Alpine Linux is suitable for Java development. BellSoft engineers have integrated the Alpine Linux port into OpenJDK, so you won’t encounter any compatibility issues. And Liberica JDK is the best Java distribution for Alpine as it comes with support from engineers who backported this OS into OpenJDK. Based on Liberica Lite and Alpine, we created microcontainers of 42.72MB, the smallest on the market! As far as technical drawbacks of Alpine Linux are concerned, the lack of dedicated commercial support means you won’t get a prompt reaction to your issue. In addition, musl is associated with inferior performance in comparison to glibc. 2. Alpaquita Linux Alpaquita Linux Benefits: Lightweight LTS releases Commercial support available musl and glibc support Optimized for Java Containers with CRaC Drawbacks: No GUI or desktop version ARM version not yet ready Not compatible with some legacy support Base container image size (compressed): 3.3MB (Alpaquita Stream musl) 8.32MB (Alpaquita Stream glibc) Alpaquita Linux is a Linux distribution developed by BellSoft. The incentive behind a new Linux was a lacking solution optimized for Java deployment on server and cloud, which at the same time solved the drawbacks of aforementioned products. As a result, Alpaquita Linux Has the base image of only 3.3 MB (musl) and 8.32 MB (glibc); Comes in two versions: with glibs and musl, the latter having two options, standard one and BellSoft musl optimized for performance; Enjoys four years of enterprise LTS support with dedicated team on duty 24/7; Includes features for enhanced security such as kernel hardening, process isolation, and userspace compilation options. In addition, BellSoft provides security advisory and regular security updates to avoid zero-day vulnerabilities; Contains tools facilitating the development of Java applications. Developers can take advantage of extensive documentation on working with various Alpaquita features (Secure Boot, OpenRC, etc.) and building efficient and secure container images. To further enhance the experience of developing and deploying cloud-native Java applications, we created Alpaquita Containers featuring Alpaquita and Liberica JDK so that developers always have regulary updated containers with fixes and patches both for JDK and the OS. And those striving for higher availability and predictable scalability in their clusters can use Alpaquita Containers with Coordinated Restore at Checkpoint support. Integrating the feature into your workflow is not complicated - follow our guide on using CRaC with Java apps in a Docker container to find out how. 3. Ubuntu Ubuntu Benefits: LTS releases Commercial support available Drawbacks: Shorter release cycle Base container image size (compressed): 27.01MB (ubuntu:20.04) Ubuntu is Debian-based and well-known as a desktop Linux distribution, and it also has a great Ubuntu Server option with LTS support from Canonical Ltd. Ubuntu Server is highly scalable and versatile and can be used for multiple purposes such as Kubernetes clusters, cloud computing, IoT, etc. LTS versions are supported for an extended period so that you can upgrade at your pace. For example, the latest 22.04 LTS release will receive updates until 2027 and Extended Security Maintenance until 2032. However, Ubuntu has a shorter release cycle than some other Linux distributions. LTS releases are made available every two years, and interim versions are released every six months. Although frequent updates are beneficial per se as they provide the developers with fresh packages and settings, the migration to the next LTS release requires more time and effort, which may be suboptimal for some companies. Ubuntu Server is free, but a commercial subscription with 24/7 support is also available. An essential feature of paid services offered by Canonical is Livepatch, which installs kernel security updates without rebooting the system. 4. Debian Debian Benefits: Stability Drawbacks: No enterprise support Delayed feature integration Base container image size: 29.93MB (debian:stable-slim11) Debian is a well-established Linux distro with 30 years of history and a primary focus on stability and security. There are no LTS releases but rather ‘Unstable,’ ‘Testing,’ and ‘Stable’ branches. Stable versions are released every two years and supported for three years. On the one hand, three development stages and an extensive review provide exceptional stability and reliability of Debian packages. On the other hand, they don’t include the latest features or cutting-edge software. Debian is 100% open-source, with a large community of volunteers working on its enhancement. But the lack of dedicated commercial support means your issue won’t be resolved quickly. You may have to wait several weeks for a bug fix, which is unacceptable in an enterprise environment. 5. RHEL RHEL Benefits: Reliable support Increased security Drawbacks: No community edition Expensive Base container image size (compressed): 10.3MB (distorless redhat/ubi8-micro:8.6) Red Hat Enterprise Linux (RHEL) is a commercial Linux distribution developed and supported by Red Hat. The company provides LTS releases and 24/7 support. RHEL boasts increased security for a reason. It includes SELinux, a set of kernel modifications defining access control for applications, files, and processes, and kernel live patching. RHEL builds also attained Common Criteria Certification, which guarantees reliability and enhanced security of IT products. There are no community versions of RHEL, but recently Red Hat introduced a build for individual developers only. If you are not ready to pay a high price for a subscription, it would be better to stick to free Linux distributions with optional support. Important update: in June 2023, Red Hat limited access to RHEL's source code. You can read more about the consequenses of this move for enterpises using free RHEL-compatible Linux distributions in the dedicated article. 6. CentOS CentOS Benefits: Community-based Drawbacks: Heavyweight Discontinued Base container image size (compressed): 79.65MB (centos:8) CentOS is a community-based Linux distribution and has been an official fork of RHEL since 2014 when Red Hat announced that it would sponsor the CentOS Project. For a long time, developers enjoyed all benefits of RHEL in free CentOS distribution. But in 2020, Red Hat discontinued CentOS development, leaving the developers with CentOS Stream, a development version of RHEL. As a result, users will get all the new features in CentOS Stream but no bug fixes or security patches, which will only be introduced into the stable RHEL version. Therefore, CentOS is not a viable option for enterprise development. 7. Amazon Linux 2 Amazon Linux 2 Benefits: Cloud-oriented Drawbacks: For AWS only Base container image size (compressed): 59.41MB (amazonlinux2) Amazon created its own Linux distribution tailor-made for AWS Cloud. It comes with enhanced performance optimized for Amazon EC2, configurations providing ideal integration with most AWS services, and many AWS tools such as Amazon CLI, which simplify administrative tasks. The company offers LTS support, ongoing security patches and bug fixes, and kernel live patching. Amazon Linux 2 is provided as Amazon Machine Image (AMI) upon creating an EC2 instance. Although having an OS developed for a specific cloud saves the trouble of manual configurations and optimizations, issues are likely to arise when switching a cloud provider. In addition, if you have a hybrid- or multi-cloud environment, it would be better to use a unified distribution. Speaking of Cloud, one of the most prevalent modern issues of Cloud deployment today is the way the costs rise when your apps become more popular and as such require more cloud resources. To help overcome this challenge, we created a white paper on multiple ways you can lower the Cloud costs. 8. Oracle Linux Oracle Linux Benefits: Full-fledged substitution for CentOS Drawbacks: History of sudden changes in licensing policy Enhanced performance with Oracle products only Base container image size (compressed): 46.5MB (oraclelinux:7-slim) Oracle Linux is a 100% RHEL-compatible distribution developed as a CentOS replacement. Oracle provides free binaries, updates, and patches and contributes to the open-source Linux project. This Linux distro is designed for hybrid- and multi-cloud workloads and comes in two versions: one with Red Hat Compatible Kernel (RHCK) and the other with Unbreakable Enterprise Kernel (UEK). UEK has optimized performance for Oracle Database and Oracle applications. Although Oracle Linux is free to download and use, the company offers optional commercial support with flexible plans. Kernel live patching and cloud-native computing tools are part of a premier commercial offering. When migrating to Oracle Linux, you should remember that the company plays by its own rules and can suddenly change its attitude towards open source solutions it offers. A product that once has been free can be suddenly commercialized for enterprise use, as in the case of Java 8. Comparative table for Linux server/cloud distributions In the table below, we summarized all the features of described Linux distributions: Feature Ubuntu Debian RHEL CentOS Amazon Linux 2 Oracle Linux Alpine Linux Alpaquita Linux Community edition ✓ ✓ x ✓ Yes, but in a bundle with EC2 instance ✓ ✓ ✓ Commercial support ✓ x ✓ x ✓ ✓ x ✓ LTS releases ✓ x ✓ x ✓ ✓ ✓ ✓ Kernel live patching ✓ x ✓ x ✓ ✓ x planned Optimized for Java x x x x x x x ✓ Ready containers with CRaC support x x x x x x x ✓ Base container image size (compressed) 27.01MB 29.93MB 10.3MB 79.65MB 59.41MB 46.5MB 2.67MB 3.3MB Linux distros comparative overview - [Six ways to lower development costs](https://bell-sw.com/announcements/2022/07/06/six-ways-to-lower-tco-in-it/): Every IT department is looking for ways to reduce its IT infrastructure costs. Costs can easily get out of control from a number of sources: expensive hardware, maintenance bills, multiple licenses, and unreliable software just to state a few. Bellsoft has already examined ways to reduce costs around Java and support in a guide to lowering TCO by choosing the right OpenJDK vendor. In this article we take cost reduction a step further and explore six additional areas and methods that will tame your expenditures. Table of Contents Microservices Migrate to the cloud Automate to the max. Monitor everything Watch your containers Clean out the waste on time Standardization Choose the right technologies Energy-efficient hardware Useful Java utilities Operating system Open-source vs proprietary software Minimize risks Virtual machines: good old classics Summary Microservices Although microservices are not a novel technology, some companies still stick to their monoliths. In cases of small or legacy applications, monolithic architecture may be more beneficial. But if you are planning to add new functionalities, scale, and expand the customer base, the costs of maintaining a monolith will eat a significant part of your budget. Microservice architecture enables a company to cut the expenses in the long-term through: Facilitated maintenance and increased productivity of the team. Small, independent services are easier to develop and debug. A team focuses on their dedicated microservice without having to entangle the intricacies of the whole application, so developers can do more in less time. Reduced downtime and failure resilience. If one microservice needs updating or debugging, there is no need to take down the whole application and risk losing the clients frustrated with service interruption. DevOps enhancement. An application broken into smaller loosely-coupled chunks is faster to build, test, and send into production. Increased agility enables the company not only to accelerate time-to-market, but also shorten release cycles and introduce incremental updates. Moreover, application deployment and management can be further automated and facilitated by means of containerization, which reduces the risk of errors and saves time and human resources. As a result, a company spends less time and money on development, but at the same time provides a fail-proof, high-quality product to the customers. But to make microservices work to your advantage, you need to design the architecture carefully and set up appropriate infrastructure. Read our guide to microservices to get a better grip on theory or use our tutorial and try building your own Java application based on this model. If you worry about compatibility issues, our Liberica JDK supports the widest range of platforms on the market and so is great for building microservices regardless of devices used by your developers! Discover Liberica JDK Migrate to the cloud Some of the obvious and immediate cost reductions associated with cloud migration are: Cost of hardware (servers, auxiliary equipment), its licensing and maintenance Rental costs related to physical data centers and office premises Utility bills for electricity, gas, coolant, etc. Corporate downsizing But cloud relocation is a serious endeavor. Although there are multiple technologies supporting cloud-native development, and cloud vendors offer ready solutions for cloud migration, you need to invest time and money into rewriting the code base, hiring or training personnel to work in cloud services, alleviating security risks associated with cloud hosting, etc. By carefully defining the strategy of cloud-native development at your company, you will avoid such pitfalls as cloud-vendor lock-in, inflated cloud bills, and under- or overutilization of resources. Below are some of the best practices of working with the cloud. Automate to the max. Modern software solutions help businesses to automate various processes such as compliance and security monitoring, updates and patches integration, deployment, scaling and so on. By turning tasks into automatic workflows, you: Save the time of your developers for them to focus on enhancing your product Minimize the risk of human error Accelerate the CI/CD pipeline Introduce more complex deployment schemes such as blue-green or canary deployment to lessen the costs of software failures You can use Kubernetes for automatic management and scaling of containers. To control corporate runtimes, use Liberica Administration Center (LAC). It enables you to manage all Java runtimes in the Windows PC fleet from a single dashboard, i.e. monitor licenses, integrate security patches, and perform updates with one click. Learn more about LAC Monitor everything Low prices for cloud resources may tempt you into the trap of neglect. Simply rolling out new instances is not always the solution to underperformance, and as a result, your application devours resources and still doesn’t meet the business requirements. Cloud monitoring is an essential part of TCO reduction strategy and rests upon the same principle: “do more with less”. Although it is a complex task requiring a separate discussion, key points are: Monitor the behavior of containers and Kubernetes pods: it is not uncommon for them to use two times less CPU than requested Check the JVM metrics regularly including garbage collection, which may impact the app’s performance significantly Retrieve Kubernetes metrics as part of cluster check-up Study latency, peak rates and app’s performance under load As a result, you will be able to define an optimal scaling strategy, utilize resources wisely, and prevent unexpected expenses coming from the necessity to allocate more resources to the app due to rapid surge of requests, for instance, on Black Friday. Watch your containers Cloud computing implies working with virtual machines (VMs) or containers instead of physical equipment. We have already written about the benefits of containers, but if you rely heavily on out-of-the-box solutions, you will waste the potential of reducing the cloud costs even more by utilizing small and performant containers. For instance, a Debian base container image size is 27.78MB, whereas an Alpaquita Linux base container image is only 3.59MB. Coupled with Liberica Lite it provides a small container for Java applications of only 52MB — imagine how much space in the cloud you can save! But size is not the only container characteristic that matters. A perfect container for your enterprise doesn’t exist yet, but you can make one yourself: Choose a fresh JDK image with fresh bug fixes and security issues and make sure it is updated regularly Tune JVM parameters such as -Xmx or GC Determine heap and memory requirements Adjust the configurations based on load testing results Continuously monitor container health in production In summary, take care of your containers and in return, they will contribute to your TCO reduction goals. Clean out the waste on time Unused data doesn’t disappear; instead, it accumulates on your instances like waste and thus deteriorates the performance of the application and takes up precious resources. Define the data retention policy. The data you or your users don’t use can be moved to cloud archives, which are cheaper than regular cloud storage. And some data can even be removed. After the data retention period expires, delete the data. Holding on to obsolete information simply for the sake of being on the safe side leads to unnecessary expenses and safety risks Clean up the database regularly and delete duplicate data Eliminate containers you no longer use. If you use Docker, stopped containers and images are not removed unless explicitly ordered, which results in extra space usage. Run docker image prune and docker container prune regularly to solve the issue. Standardization Standardization of IT infrastructure is aimed at reducing the complexity and total cost of systems used at the company. It covers Software. Instead of paying several JDK vendors, choose one whose runtime is compatible with all your platforms and thus reduce cost of support and time spent on monitoring the security and updates. Hardware. Maintaining both Aarch64 and x86 devices is more costly than using one architecture across the whole organization. Procedures. Standardized stack of frameworks, utilities, and programs promotes cooperation between teams following the same procedures, increases the security, and accelerates the time-to-market. A good practice for a software company is to create a single container that suits all business needs and integrate it into all production stages, from development through testing to deployment. This way, you will minimize the number of vendors you work with, eliminate compatibility issues and save time of your developers. BellSoft knows the value of standardization and offers a unified technology stack to Java developers: Liberica JDK, a unified runtime with the widest range of supported platforms and three flavors for different purposes Liberica NIK for creating native images LAC for automated runtime monitoring and updates JFR and Mission Control for performance profiling Choose the right technologies Energy-efficient hardware Energy consumption is one of the most relevant topics in all industries because it is related both to high TCO and environmental pollution. The IT sector is no exception. For instance, in high-performance computing, the cost of a supercomputer is similar to the sum of energy bills over its lifetime. The same applies to any high-performance hardware: the reduction of time-to-solution leads to the increase of energy-to-solution. Modern energy-efficient technology and appropriate design approaches can reduce energy consumption by 25%, server consolidation — by 20%. The combination of less power-hungry hardware and performant software will yield the best result. If you develop Java applications, Liberica JDK is compatible with the widest range of platforms, so it can be used with any server, cloud, or energy-efficient hardware of your choice. Useful Java utilities Using Java for enterprise development gives access to an extensive ecosystem of solutions aiding in overall improvement of app’s performance and memory consumption: Java Flight Recorder and Mission Control are used to profile and monitor Java applications and detect memory leaks, which lead to increased resource consumption Embedded Java databases like Daffodil DB help avoid compliance issues and are tuned together with other JVM settings thus reducing administration time Spring is a family of frameworks, where every developer will find a solution for their needs. In addition, Spring Native comes with embedded support for Liberica NIK, which enables the reduction of startup and memory consumption of Spring Boot applications. Find out how to reduce TCO of Spring apps with Liberica JDK in our white paper on the topic. Reduce TCO of Spring apps! There is one more special technology developed by BellSoft: Liberica Native Image Kit (NIK). Liberica NIK is a tool that converts Java applications into small native executables thus minimizing memory footprint and accelerating startup. It supports multiple system configurations and programming languages, so it is perfect for microservices. Our tool is based on the Community Edition of GraalVM and is free to download and use. Check out our guides on using Native Image Kit with various frameworks: Spring Native, Quarkus, and Micronaut. Or try it out now and see how the startup of your app reduces to 1/10 s! Discover the possibilities of Liberica NIK Operating system Another factor contributing to TCO reduction strategy is the choice of an operating system. The TCO for an OS includes: Installation and maintenance. Open-source solutions are cheaper than proprietary ones, but it is advisable to opt for products with optional commercial support to mitigate the risks and promptly deal with bugs and security issues. Support period. Some OSs have to be upgraded every couple of years, while other solutions enjoy long-term support from their vendors. In addition, some products require an upgrade of hardware leading to significant expenditures. Customization. The opportunity to customize an OS may be beneficial for some companies that evolve rapidly or have specific needs, but don’t want to be limited by end-user license agreement (EULAs). Downtime. Opt for products with high-availability because downtime has severe and costly impacts on business, from loss of customers to broken SLAs. The most optimal OS that helps to meet the goals within the above mentioned aspects is Linux. Some studies have shown that Linux-based solutions are 26—36% (in case of free Linux distributions) and 19—27% (in case of Red Hat distribution) more cost-efficient than Windows-based ones. Read our review of Linux distributions for server and cloud to select an optimal solution. The exciting news is that BellSoft created a lightweight Linux distribution tailor-made for Java applications, Alpaquita Linux. It is based on Alpine, but comes with a bunch of performance and security enhancements, as well as LTS support from BellSoft. What is more, we provide two Alpaquita variants based on optimized musl and glibc, so you don't have to worry about migration issues. Learn more about Alpaquita Linux Bundled together with Liberica Lite and Liberica NIK in a microcontainer. Alpaquita is an end-to-end solution optimized for creating and deploying Java apps. Imagine how much you can save by using Linux and JDK stemming from one vendor and with LTS support for both! Open-source vs proprietary software One way to reduce TCO dramatically is to opt out of proprietary software in favor of free open-source solutions. Although open source contributes to short-term cost reduction, which is an acquisition price, TCO is about long-term savings, ie., the price of maintaining the software. This is where hidden dangers of open source sit: Legal risks. Developers may accidentally pull licensed dependencies and violate copyright, which will lead to litigations and vast expenses Safety risks. Not all open-source products receive timely security updates, which means that your project may contain known vulnerabilities and be vulnerable to attacks. In addition, open source packages can be modified by anyone, and in rare cases, open software may contain malicious code. Operational risks. Many open-source products are maintained by the community and have no dedicated support. It means that in case there is a bug in the product you are using, you have to fill in the form and wait for your issue to be solved, sometimes for a couple of weeks. It is inacceptable for enterprise-grade technologies. It doesn’t mean that all open-source products are inherently unsafe. The key is to find the right balance between free and proprietary technologies and choose open-source solutions from reliable vendors, with regular updates and optional commercial support. Liberica JDK is a free Java runtime developed by BellSoft, a major OpenJDK contributor. You can use our binaries for free and stay safe and up-to-date with the quarterly CPU-release cycle, but you can also choose our High-Powered Support, which costs five times less than that of Oracle’s and provides you with off-cycle updates and response times within 24 hours based on SLAs. View support prices Minimize risks TCO includes hidden costs such as disaster recovery (after data loss or cyberattacks) and loss of clients or reputation. They are hard to calculate but always massive and should be accounted for in business strategy through risk mitigation. IT companies can do the following to minimize risks related to their product: Reduce downtime by regularly checking the health status of hardware and software, choose software products (not only OS) with high availability. Define and follow SLA. Service Level Agreement (SLA) is an agreement between a company and commercial customers that details the company’s obligations towards users. An SLA should be carefully crafted with participation of developers, not only lawyers, because only your engineers know how to deliver services basen on the SLA. A realistic SLA is a prerequisite of clients’ satisfaction. Perform backup and protect your data. If one server or microservice goes down, you must have an opportunity to get it back online ASAP. Three leading causes of data loss are hardware or software failure and human error. Ensure enhanced security of the whole IT infrastructure. Make sure that your hardware is protected physically and with passwords, and your software receives regular security patches eliminating known vulnerabilities that can be used by hackers. Never use software dependencies from unknown sources or without fresh patches. Work with vendors providing reliable support. If there is a problem with any technology, it has to be solved promptly. For instance, our support team provides 24/7 access and security patches within 48 hours, and responds within one hour. Virtual machines: good old classics If you are not ready to shift development to the cloud, you still can reduce TCO through clever employment of hardware. Instead of underutilizing powerful server machines, virtualize them. If one physical server hosts ten virtual ones, instead of buying twelve servers you acquire only two (one for the backup purposes), and thus save min. 50% of initial acquisition cost plus minimize expenses for power and cooling. The best operating system for virtual machines is Linux Server distribution. If you later decide to have a hybrid cloud application, i.e., part of the data is kept in physical data centers, another part in the cloud, you will benefit most from a Linux distribution suitable both for virtual machines and cloud computing, because, as we mentioned before, less vendors means less licensing expenses. A Linux distribution soon to be marketed by BellSoft supports both virtualization and cloud technologies. Being even smaller than Alpine Linux, it will enable you to minimize the size of containers and VMs and save even more! Summary To sum up, there is no template of TCO reduction strategy as every business situation is unique. Nor are there any definite numbers in terms of savings. But following the “doing more with less” principle you will reduce Acquisition costs Maintenance Staffing costs Lost user productivity costs BellSoft offers a unified stack of Java technologies enriched with Alpaquita Linux helping you to reach the goals described in this article: Liberica JDK, which supports the widest range of platforms (including modern energy-efficient devices), receives regular patches and updates, and comes in a lightweight version, is perfect for microservices and microcontainers Liberica Administration Center provides you with automated monitoring, license control, and updates of Java runtimes through the organization Liberica Native Image Kit helps to minimize resource consumption and accelerate startup Our soon-to-be-released Linux is suitable for cloud, servers, and VMs Our engineers provide LTS support both for JDK and Linux with fast response times and fixes - [Faster time to market with Liberica JDK and best DevOps practices](https://bell-sw.com/announcements/2022/07/13/faster-time-to-market-with-liberica-jdk-and-best-devops-practices/): How to reduce software release cycles with Liberica JDK and DevOps In modern software development, faster time to market defines a company’s success. Imagine you spend three years developing a perfect application with top-notch features and enhanced security, with every possible bug tested for and eliminated, and then your competitors launch a similar product, even several ones! Let’s reverse the situation: you’ve managed to speed things up (making your Dev and Ops teams arch enemies in the process) and launch a one-of-a-kind app. But your “high-end” product turned out to be a failure because it lacks a user-friendly interface or crashes every time a user pushes the wrong button. How can you accelerate development without sacrificing the quality or security? How can you break a deadlock of blame-shifting between teams and make them work towards the same goal? How can you choose the right tools to increase the efficiency of deployment and reduce expenses—simultaneously? In this article, you will learn how to: Go from shipping a product once in three years to once in three weeks Provide product quality and security at all development stages: from code writing to production Transform the mindset of your teams to collaboration instead of rivalry Use Java technologies to speed up the deployment, enhance the security of your app, and reduce the TCO In the beginning there was Agile “Production first” mindset “Shift left” approach Quality testing Security monitoring Express delivery with Liberica JDK containers Conclusion In the beginning there was Agile We are used to thinking that agile development is an indispensable component of DevOps. It is true, but you may be surprised to hear that you should start thinking about DevOps implementation only when you are already good at agile development. Why? Because agile development guarantees that you have a high-quality product ready to be shipped. A bad product delivered fast is still a bad product! You don’t want to lose the trust of your customers, so think about the quality before accelerating time to market. The fundamental principles of Agile methodology were expressed in the Manifesto for Agile Software Development: Individuals and interactions over processes and tools. If your team doesn’t understand why they have to integrate changes into their workflow, they will be useless. Explain to them the benefits of CI/CD and the importance of delivering value to the end-users. Listen to their concerns and offer solutions, and they will be more willing to follow the new practices. Working software over comprehensive documentation. Product documentation is necessary for people to understand how your software actually works, but it doesn’t substitute the real application. Focus on creating the former one and write the documentation on the sidelines. Customer collaboration over contract negotiation. Think of your customers as your partners. Their success is your success. The moment you adopt this attitude, you will start treating the development process differently. Designing more intuitive and user-friendly software is difficult, but you only have to do it once, whereas the customers have to pay the price for your laziness every time they use your product. Responding to change over following a plan. Agile development is all about flexibility. Don’t be afraid to introduce changes into the project plan to react to the shifts in technologies, environment, or customers’ needs. Even if you have to substitute the Product Manager who doesn’t grasp the stakeholders’ needs and find a person with a clear vision — do that! It won’t be long before you see the improvements. If you implement the above methods correctly, you will naturally come to shipping incremental releases every 1—4 weeks based on the feedback from the users. “Production first” mindset Classic software development model is based on the waterfall methodology. In this model, the process of building a product is broken into sequential phases: analysis, design, implementation, and testing. The team must conduct a thorough research, document all the features in advance, and follow the backlog strictly, bringing one stage to an end before moving on to the next one. The waterfall model enables the company to deliver high-quality software, but the problem lies in its inflexibility. For example, a team has been working on a big lump of software for nine months: writing the code, adding new features, and getting to the bottom of the product log. At some point, they were code-complete and ready to ship. However, it turned out that they had another high mountain to climb, i.e. to deploy the code into production. As nobody thought about production beforehand, the team faced a bunch of problems: The security features were not in place, and some vulnerabilities sneaked into the finished product The application wasn’t scaled in such a way to fit into the existing infrastructure There was no understanding of how to deliver a product to end users If the team had thought about delivery before writing a single line of code, they would have the idea of fitting the app into the infrastructure and getting it into production. The idea behind the “production first” mindset is to think the entire pipeline through before starting the work on the application! Start with writing the infrastructure as code (IaC) files. IaC is a powerful instrument that Facilitates the creation of temporary environments that can be built, used, destroyed, and rebuilt as needed, thus saving your company a tremendous amount of money Enables the process automation and, as a result, takes the responsibility off a single engineer. Imagine that a person in charge of providing the environment got sick or is on vacation, meaning the product shipment is delayed. With IaC, the process of VM provision and thus the product deployment can be automated Aids in disaster recovery. If your deployment is automated and IaC is in place, you can simply run another pipeline instead of a crashed one by changing a couple of parameters, so your infrastructure, including your application, will be back in a matter of seconds Lay down the IaC pipeline from repository to production before starting to design the application. This way, the developers will already know the infrastructure capacity and the environment and build the product accordingly. In addition, this mindset eliminates the rigidity of the waterfall model as the very first commit made by a developer starts moving down the pipeline towards production. After that, you simply add value on top of value without worrying about the delivery process, having already created the road to production. “Shift left” approach Product quality and security is every developer’s responsibility! Shifting left in software development means switching convictions like this: Taking responsibility is worth it! If your developers don’t think about quality and security, their code becomes very brittle and difficult to change if problems are identified later. On the other hand, engineers who have to test their code themselves are more attentive to possible weak spots. It helps them avoid constructions like nested if-statements and stick to a shortcut logic, which is easier to test. Below you will find out what tests to integrate into the developers’ routine work. Quality testing There are three major test categories aimed at evaluating the product quality: Unit tests protect the developers from the mistakes they would otherwise make. They are like an insurance policy. Nobody thinks about them while everything runs smoothly. And then, when a critical bug escapes into production and sets a company back hundreds or millions of dollars, it is too late to regret not having written unit tests. Integration tests make sure that software modules interact correctly. Even though a testing team performs them, developers should keep them in mind and make their colleagues’ life easier by monitoring the code and throwing errors as close to the problem as possible. In this case, issues are easier to troubleshoot. Load and performance tests enable you to analyze how the app behaves under load and whether it satisfies the customers’ needs. In addition, these tests ensure that the application performs well under different conditions and in different environments/OS. In short, the above tests measure different data but with one purpose: to verify the top quality and performance of the product in accordance with the stakeholders’ requirements. BellSoft implements the following testing procedures as part of Liberica JDK QA/QC: Technology Compatibility Kit (TCK) testing Functional and regression OpenJDK testing Proof-of-concept (PoC) exploits as part of security evaluation Benchmark testing These tests aim to demonstrate that our distribution is compliant with Java SE standards, free from security issues, and performant. Security monitoring Security monitoring is just as important as quality validation, if not more. A feature that doesn’t function correctly is bad. Stolen data is a catastrophe. Ensure the security of a product from the ground up, from the repository on the developer’s PC and all the way into post-production. Start with the machines. Ensure that the PCs have a two-factor authentication, encrypted hard drive, and a VPN. This way, it will be extremely difficult to get the data from the computer. Scan code, IaC, and containers for vulnerabilities. Security scanning can be performed manually, but it is better to use vulnerability scanners that enable the developers to automate the process and receive timely security notifications. Ensure post-production safety monitoring and integrate security patches as soon as possible. For instance, at BellSoft, we follow the practice of quarterly CPUs (updates with only critical patches) for LTS releases and the OpenJDK release cycle keeping Liberica JDK safe at all times. Using a unified Java runtime from a trusted vendor will also benefit the safety of your development. Bellsoft is fully responsible for Liberica JDK. Apart from CPUs, we provide hotfixes for our clients and integrate the changes into the main OpenJDK branch to be available to everyone. Moreover, BellSoft is a member of the OpenJDK Vulnerability Group and Java Community Process Committee, which means that your runtime is free from common vulnerabilities. Get the white paper on Liberica JDK Express delivery with Liberica JDK containers In our previous article How to make Java the DevOps best friend, we discussed the benefits of a good JDK for DevOps practices. Here, we will focus on one particular topic, the containers. So you laid down the express pipeline; now it’s time to think about shipping the app into the virtual environment for testing or production. Let’s be honest: a smooth modern highway is useless if you use carts and donkeys to travel on it. Good containers are like race cars: fast, secure, and carry significant potential under the hood. Besides, the smaller the container, the fewer resources it consumes, which is directly proportional to costs in cloud development. Moreover, every day developers perform multiple push and pull operations, whether to test a code or send it into production. Waiting for 30 seconds for a container to load would drive anyone mad! BellSoft keeps up to date with the latest trends. We were one of the first vendors to understand that containerization is the future of the IT industry which is why we focused on developing small and performant containers. Bellsoft’s smallest container based on the lightweight Alpine Linux weighs only 42,72 MB, so the pull/push time is significantly reduced. Besides, you can deploy numerous containers even if your cloud resources are limited and thus decrease overall costs. We have three Liberica JDK flavors: Full, Standard, and Lite. The Lite version is a full-fledged Java SE compatible runtime optimized for dense cloud deployments. To minimize resource consumption and static footprint, even more, we created Liberica Native Image Kit. This tool allows you to create seamless multilingual projects and reduce the startup time to 1/10 second! BellSoft containers are always up-to-date in terms of security, so you don’t have to worry about downloading a container with an outdated JDK or known vulnerabilities. Truth to be told, Alpine Linux is good for reducing container size, but its developers didn’t have Java in mind when creating it. You would benefit even more from the OS designed specifically for Java deployment. But is there such a system? The answer is: yes! BellSoft has created a perfect solution for Java developers — the only Linux tailor-made for Java, Alpaquita Linux. It is even smaller than Alpine, but comes with Enhanced security Optimized performance LTS support Utilities for Java development Alpaquita Linux will bring the Java experience at your company to a new level. Coupled with Liberica Lite and Native Image Kit, it represents an end-to-end solution for cost-efficient and smooth development and deployment of cloud-native applications. Learn more about Alpaquita Linux Conclusion As you can see, accelerating time to market is not about working after hours and loading developers with tasks up to the eyebrows. It is about thinking in advance, being flexible, sharing responsibility, and using the right technologies to boost the performance and security CI/CD pipeline. And remember: your ultimate goal is to generate value. Build your corporate culture around this concept, and receive appreciation from your team, partners, and customers. At BellSoft, we are committed to helping you achieve better outcomes. Discover our products aimed at helping you reduce expenses, free the time of your developers, and enhance corporate DevOps practices: Liberica JDK is a unified Java runtime with the widest range of supported platforms on the market High-Powered Support that delivers fixes within 24 hours based on SLA and costs five time less than that of Oracle’s Liberica Administration Center is a tool that enables you to automate monitoring and updates of all Java runtimes within your Windows PC fleet Liberica Native Image Kit is perfect for creating microservices as it supports multiple languages and configurations and optimizes RAM consumption - [Liberica 18.0.2, 17.0.4, 11.0.16, and 8u342 builds are generally available](https://bell-sw.com/announcements/2022/07/21/liberica-18-0-2-17-0-4-11-0-16-and-8u342-builds-are-generally-available/): Today we announce a Critical Patch Update (CPU) of Liberica JDK versions 8u341, 11.0.15.1.1, and 17.0.3.1.1. CPU patches contain fixes for Common Vulnerabilities and Exposures (CVE) and help to keep the runtime secure and performant at all times. In addition, we release PSU versions (18.0.2, 17.0.4, 11.0.16, and 8u342) with non-critical fixes. The release contains 848 fixes and backports overall. BellSoft participated in eliminating 34 issues (32 in JDK and 2 in FX) in all releases. Contents How to keep your runtime secure The summary of fixes List of security issues fixed Summary of fixes in Liberica JDK Notable upstream changes Supported platforms Enjoy the most stable runtime! Useful links How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 4 security issues (CVEs) fixed 31 total security fixes in CPU release: in Liberica 8u341: 9 security fixes + 2 in FX in Liberica 11.0.15.1.1: 8 security fixes + 2 in FX in Liberica 17.0.3.1.1: 8 security fixes + 2 in FX In addition, PSU releases include a total of 817 bugs and backports fixed: in Liberica 8u342: 11 security fixes (9 + 2 in FX) + 84 additional fixes in Liberica 11.0.16: 10 security fixes (8 + 2 in FX) + 287 additional fixes in Liberica 17.0.4: 10 security fixes (8 + 2 in FX) + 270 additional fixes in Liberica 18.0.2: 13 security fixes (11 + 2 in FX) + 132 additional fixes Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2022-34169 7.5 xml java.xml network low none none unchanged none high none CVE-2022-21540 5.3 core-libs java.base network low none none unchanged low none none CVE-2022-21541 5.9 core-libs java.base network high none none unchanged none high none CVE-2022-21549 5.3 core-libs java.base network low none none unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE CVSS 8 11 17 18 CVE-2022-34169 7.5 ● ● ● ● CVE-2022-21540 5.3 ● ● ● ● CVE-2022-21541 5.9 ● ● ● ● CVE-2022-21549 5.3 - - ● ● Notable upstream changes This CPU release contains a number of important additions and updates. Customizing the generation of a PKCS12 keystore In Java 8, the KeyStore.load API allowed the supplied password to be null. This value was to signal the skipping of the keystore integrity check. Yet when the password was null, the PKCS12 implementation returned no certificates. This behavior was fixed. Infinite loop in ZipOutputStream.close() In Java 11 and 17, in some cases, when the client disconnected or the socket write timed out, the closing of the underlying output stream happened too soon, and the zip file could not be completely written. It led to the infinite loop in ZipOutputStream.close() loop. Issues with cpu.shares There were two fixed issues with cpu.shares in the container environment. The first one was about incorrect calculation of the number of CPUs for the processes to use, which could result in CPU underutilization and some unexpected behavior. The second one was related to the faulty computation of ActiveProcessorCount, which in turn made the JVM use only some of available CPUs. Lambda deserialization failed for Object method references on interfaces Deserialization of serialized method references to Object methods that used an interface as the type on which the method was invoked became possible again. Note that the class files must be recompiled to allow the deserialization. Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK Useful links [JDK-8266526] Customizing the generation of a PKCS12 keystore [JDK-8283522] Infinite loop in ZipOutputStream.close() [JDK-8283355] cpu.shares does not correctly calculate the number of CPUs for the processes to use [JDK-8288604] cpu.shares did not compute ActiveProcessorCount correctly [JDK-8288605] Lambda deserialization failed for Object method references on interfaces - [Enhancing JVM monitoring with Prometheus on AWS](https://bell-sw.com/announcements/2022/07/21/enhancing-jvm-monitoring-with-prometheus-on-aws/): This article is the fifth part of the cloud-native microservice development in Java™. In the first part of the series, we designed the microservice architecture and developed two microservices for our simple Java e-commerce application. In the second part, we containerized our applications using the cloud-native buildpack implementation paketo.io., which is natively supported by Spring Boot. We created a container image of our microservices using Liberica JDK, a progressive Java runtime from a major OpenJDK contributor. Discover Liberica JDK In the third part, the container image was published in the AWS Container Registry ECR and deployed on the managed Kubernetes service of Amazon Cloud (EKS). We also demonstrated our microservices in action using the Postman collection. In the fourth part, we configured custom domain names for our microservices. We also secured the web traffic using TLS 1.2+ and applied logging using Fluent Bit and CloudWatch. In addition, we performed monitoring using CloudWatch Container Insights. Nevertheless, monitoring of cloud-native microservices is a vast and important topic. This article provides a tutorial on using the Prometheus monitoring tool with microservices running on EKS Kubernetes cluster. Kubernetes cluster monitoring and TCO reduction JVM monitoring with Prometheus on AWS Enable Prometheus in Spring Boot apps Amazon Managed Service for Prometheus (AMP) Set up AMP Workspace Set up Prometheus Metrics Collector Install a Prometheus server Set up IAM Role Visualizing metrics with Amazon managed Grafana Conclusion Kubernetes cluster monitoring and TCO reduction For the C-Level executives and higher management, keeping the TCO on public cloud low is a key criteria. Most of the time, running the Kubernetes cluster takes significant costs. In addition, choosing the right size of the Kubernetes worker node is important: we need to keep the costs low, but at the same time have a large enough node to run the job efficiently. With JVM monitoring, we can easily have an in-depth view of the JVM runtime in the Kubernetes. It will allow us to save the cost of picking the right kubernetes node size. In addition, understanding the behavior of the application will give you grounds for solving the issues you may encounter in the cloud. Kubernetes also gives us a declarative way to scale our application using pod replication. The replication provides a very convenient way to improve the availability and fault-tolerance of our application. The tools we will analyze in this article, Prometheus with Grafana, give us the opportunity to monitor all the pods in our EKS cluster. JVM monitoring with Prometheus on AWS If we run a Java application (or any JVM-based application) in a container, then the application actually runs on JVM. Thus, if we only monitor the container, we cannot get the full picture of the application. For better observability, we need to monitor the JVM runtime along with the container. One of the limitations of the Amazon CloudWatch Container Insights is that it can only collect metrics and perform alerting on the container level, but not on the JVM level. There are several tools we can use to monitor JVM-based microservices in the Kubernetes environment. One of the best among them is Prometheus. Prometheus is a widely used monitoring and time series database. It is part of the Cloud Native Computing Foundation (CNCF). Also, with 40k+ GitHub starts, it is the most popular open-source monitoring tool. Prometheus offers very efficient storage using a time series database, many integrations, powerful queries using the PromQL query language, great visualization, and alerting. Unlike CloudWatch, Prometheus is a pull-based monitoring system that actively collects or scrapes monitoring data from the application exposed via the Metrics API. In the cloud-native microservice application development, pull-based monitoring systems like Prometheus can have some advantage over push-based monitoring as they generate less traffic in the network. There are two ways Prometheus scrapes the metrics data. With the first approach, apps expose the Metrics API using the client library. The client library also enables developers to expose the application specific business metric (counter, gauges) to the Prometheus monitoring. This method is used for monitoring developed apps/services. The other option is Exporter-based where an Exporter running alongside an application exposes the metrics via API. This method is used to monitor third-party applications like a database. We will use client library based scraping to monitor our microservices. To activate Prometheus monitoring for AWS, we need to update our Spring Boot applications. Enable Prometheus in Spring Boot apps In Java applications, there are several ways to expose the monitoring endpoints for an application. These monitoring endpoints are used to collect application metrics information including Prometheus data to interact with JMX beans in order to expose the health endpoints. In the Spring framework, Spring Boot Actuator is used to expose these production-grade monitoring endpoints in any Spring MVC application. For more information about Spring Boot Actuator, study the official Spring documentation. If you have only just joined us, feel free to pull the ready microservices from the GitHub repository or use your own application. In our Customer microservice, we need to add the following dependency for Prometheus to the build.gradle file: io.micrometer:micrometer-registry-prometheus. We also need to add the following configuration to expose the Prometheus Metrics API: management: endpoints: web: exposure: include: health, prometheus, info, metrics health: show-details: always metrics: tags: application: MonitoringCustomerMicroservice In the next step, we need to make our application running on the pod auto-discoverable so that Prometheus can auto-discover and scrape the Prometheus metric from the scrape endpoint. For this, the following “annotations” section is added to the “esk-deployment.yaml” file: template: metadata: labels: app: microservice-customer annotations: prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/customer/actuator/prometheus" Now, we need to create a Prometheus namespace in our EKS Kubernetes cluster: kubectl create namespace eks-prometheus-namespace --kubeconfig ~/.kube/config We now need to create a docker image and deploy it in the EKS cluster namespace “eks-prometheus-namespace” as described in the previous part of the series. Once the application is successfully deployed, we can check whether the Prometheus endpoint is running by visiting the Actuator endpoint: Actuator endpoint The Prometheus endpoint shows the following Prometheus metrics: Prometheus endpoint Repeat the above steps for Order microservice to enable Prometheus there as well. Amazon Managed Service for Prometheus (AMP) Now that we enabled Prometheus monitoring in our microservices, it’s time to move back to the cloud. AWS offers Prometheus compatible monitoring and alerting service to monitor containerized applications and infrastructure at scale. With Amazon Managed Service for Prometheus, we can use the open-source Prometheus query language (PromQL) to monitor and alert to the performance of containerized workloads, including JVM running in a container. One of the biggest advantages of Amazon Managed Service for Prometheus over self-managed Prometheus is that AWS manages and automatically scales the ingestion, storage, alerting, and querying as workloads vary. The other advantage is that it is integrated with EKS, ECS, and AWS distro for OpenTelemetry. Set up AMP Workspace AMP Workspace is the conceptual location to ingest, store, and query the Prometheus metrics for a Project/Application. Thus, AMP workspace helps to isolate different project/application monitoring in Prometheus. We can create an AMP Workspace by using either AWS CLI or AWS Console. In the Amazon Prometheus, we can create an AMP Workspace as shown below: AMP Workspace Once created, we can see the details of the created Prometheus Workspace in the AWS Console: Prometheus Workspace in the AWS Console Set up Prometheus Metrics Collector The AMP does not automatically scrape operational metrics. For this, we need to configure the Prometheus Metrics Collector. The Metrics Collector scrapes the operational workload from the containerized Java microservices running in the cluster and sends them to the AMP Workspace. Collecting the metrics There are several ways to deploy a Prometheus Metrics Collector in AWS: Prometheus server or OpenTelemetry agent. In this example, we will use the AWS distro for OpenTelemetry Collector/ Prometheus server in the cluster to collect the metrics data. Install a Prometheus server We will install a new Prometheus server using Helm to ingest the Prometheus metrics data. Helm is the popular package manager for Kubernetes. It is a very convenient way to find, share, and use software built on Kubernetes. First, we need to install Helm in our local machine as described in the official documentation. In the next step, we need to add a new Helm chart repository for Prometheus: helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo add kube-state-metrics https://kubernetes.github.io/kube-state-metrics helm repo update Set up IAM Role We need to set up the IAM roles for service account for the ingestion of metrics from Amazon EKS Cluster. The Prometheus server sends the Data using HTTPS. The data must be signed using valid AWS credentials and the AWS Signature Version 4 algorithm to authenticate and authorize each client request for the managed service. The requests are sent to an instance of AWS signing proxy which will forward the requests to the managed service. The AWS signing proxy can be deployed to an Amazon EKS cluster to run under the identity of a Kubernetes service account. With IAM Roles for Service Account (IRSA), one can associate an IAM role with a Kubernetes service account and thus provide AWS permissions to any pod that uses that service account. This follows the principle of the least privilege by using IRSA to securely configure the AWS signing proxy to help ingest Prometheus metrics into AMP. A detailed description of how to set up the IAM Roles for Service Account to use AMP can be found in the user guide. After creating two roles (one for ingesting metrics data and the other for querying metrics data), we can verify the rules by searching the roles in AWS Console as shown below: Verifying the roles Helm enables us to overwrite the Kubernetes configuration with a value file. We need to create a value file with the name “prometheus_eks_values.yaml”: serviceAccounts: server: name: amp-iamproxy-ingest-service-account annotations: eks.amazonaws.com/role-arn: ${IAM_PROXY_PROMETHEUS_ROLE_ARN} server: remoteWrite: - url: https://aps-workspaces.${AWS_REGION}.amazonaws.com/workspaces/${WORKSPACE_ID}/api/v1/remote_write sigv4: region: ${AWS_REGION} queue_config: max_samples_per_send: 1000 max_shards: 200 capacity: 2500 In this file, we have configured the Amazon Managed Service for Prometheus as a target where the Prometheus server running on the EKS Kubernetes will send the scrape monitoring data. Now, create the Prometheus server using the following Helm command: helm install prometheus-chart-eks prometheus-community/prometheus -n eks-prometheus-namespace -f prometheus_eks_values.yml Visualizing metrics with Amazon managed Grafana Grafana is an open-source analytics and visualization web UI. It provides a dashboard containing charts, graphs, and alerts when it is connected to a supported data source. Grafana can be helpful in many use cases (logging, monitoring, tracing) with various tools. It is also used in tandem with Prometheus to provide a dashboard for Prometheus data on AWS. Amazon provides a fully managed Grafana. Below are the steps to set up the Amazon managed Grafana to visualize our Prometheus monitoring data. For user authentication in the managed Grafana, we will use the AWS SSO. If the “AWS Organization” is enabled, then you can create an AWS SSO user as shown below: Creating an AWS SSO user The next step is to set up the managed Grafana workspace. The Grafana workspace is a virtual Grafana server used as a unified dashboard for different data sources. From the “Amazon Grafana” > Workspaces, a new workspace can be created by selecting the “Create workspace”: Creating a workspace Now, provide the workspace name in the following console page. We gave the name “prometheus-metrics” as the workspace name: Providing the workspace name Next, we need to configure our workspace. For authentication access, we choose AWS SSO as authentication method, so that our previously created WEB SSO account can be used to log in to Grafana. For “Permission Type”, we have selected “Service managed” so that AWS automatically provides the permission. Configuring the workspace In the final console page, we set the “IAM permission access settings” as “Current Account”. In the section “Data Sources”, we can select different data sources so that we can have one Grafana dashboard for various purposes with various tools (e.g. Prometheus Monitoring, AWS CloudWatch Monitoring, AWS X-Ray Tracing, etc.) We selected “Amazon Managed Service for Prometheus” as a data source. As a notification channel, we selected “Amazon SNS”. Once our Grafana workspace is created, we can assign our previously created AWS SSO user to Grafana workspace as shown below: Assigning a user to the workspace IAM permission access settings Now, we can login to the Grafana workspace using the workspace URL and AWS SSO. After successful login, we can add our Prometheus workspace as “Data Source”: Adding the data source In the data source, we need to provide our Prometheus Query URL endpoint without the “/api/v1/query”. In addition, we enabled the “SigV4 auth” in the “Auth” section. In the “SigV4 Auth Details”, we selected our region “eu-central-1” as region and “AWS SDK Default” as authentication provider: Data source settings Next, we need to add a dashboard to monitor our Spring Boot application running on EKS Kubernetes cluster. In grafana.com, there already exists a dashboard to monitor the Spring Boot APM. We can import it using the import id “12900”: Importing a dashboard After a successful import, we will see the following dashboard with Kubernetes monitoring data including JVM specific monitoring information like CPU usage or heap used: Spring Boot APM dashboard There is also additional JVM monitoring information including GC information: JVM monitoring metrics In addition to the JVM monitoring, we can perform the in-depth Kubernetes monitoring using the Prometheus and Grafana. By adding a Grafana dashboard for Kubernetes, we can see the metrics of the Kubernetes cluster (number of nodes, pods, cluster data). We can also search the metrics for a time period. Here is a snapshot with the number of replicas running in our cluster for the namespace “eks-prometheus-namespace”: Cluster replicas data We can also see the history of the replica with the following search criteria: Replica history The above diagram shows that all our replicas (4 for our microservices and 4 for the Prometheus cluster) are running without any failure. In the production system, it is very critical to set an alert in case there is an issue with the application. Alerting enables the production support team to react quickly if there is a prod issue. Setting an alert is essential for fulfilling the SLA/SLO of an application. Here are some use cases for alerting: CPU is used over 80% Memory is used over 80% Pods are restarting every 10 minutes Database is 90% full When an alert condition is met, a trigger is usually activated to inform the production support team or security team. Depending on the alert, one or more of the following triggers are used: email, SMS, chat. Amazon Managed Service for Grafana allows setting the alerts by specifying alert rules. Conclusion We demonstrated how to implement the JVM monitoring in a cloud-native way using the leading cloud provider AWS and the most popular open-source monitoring tool Prometheus. Please note that JFR can also be used for JVM monitoring and diagnostics, but it is used mainly for non-cloud-native applications. If you want to learn more about JFR, you can read our post about JFR with code examples. In modern cloud-native enterprise software development, monitoring is an intricate part of TCO reduction strategy. In case your application underperforms or your cloud bills bloat, you have to know what went wrong, and monitoring is a great way to do that. If you are looking for other ways to reduce TCO of Java applications, check out our articles dedicated to the topic: Six TCO reduction practices for IT companies Optimizing cloud costs BellSoft strives to make your Java experience as smooth and profitable and possible: We created the smallest containers on the market for you to save precious cloud resources Our GraalVM-based utility, Liberica Native Image Kit, helps to accelerate the startup of applications up to 1/10s Our High-Powered support costs 5 times less than that of Oracle’s and is there for you 24/7. Receive help from our experts within 24 hours, off-cycle fixes, and emergency security patches Start your journey with us by trying out Liberica JDK, a 100% open-source and free OpenJDK distribution with the widest range of supported platforms. Download Liberica JDK - [Alpine Linux: small and powerful distro for Docker images](https://bell-sw.com/announcements/2022/08/03/alpine-linux-small-and-powerful-distro-for-docker-images/): The base image size of Alpine Linux is only 2.67MB, which is ten times smaller than the most popular Linux distributions, Ubuntu and Debian. And yet, it is a full-fledged Linux environment that provides you with a lightweight server solution for virtualization or containers. If you are tempted to cut the size of your OS but are unsure whether Alpine provides all the necessary functionalities, this article is for you. We will explain what makes Alpine so minuscule yet powerful, in which cases it may be suboptimal, and offer an alternative enterprise-grade solution. Table of Contents Inside Alpine Linux musl BusyBox OpenRC and apk tool Benefits for Docker containers Small images Speed Security Possible drawbacks Performance No commercial support Challenging migration and compatibility issues Useful links Inside Alpine Linux Alpine was created as minimalistic as possible thanks to Linux flexibility while preserving all the core functionalities. Developers can add packages they require leaving unnecessary dependencies out and keeping their distro clean and concise. At the same time, Alpine is not only about cleaning up the clutter, as several distinguishing features contribute to its small size. musl Alpine Linux is built around musl as opposed to other popular distributions based on glibc. musl is a C library implementation developed with minimalistic design in mind. Project members say it will be finished when there’s nothing else to remove. Contrary to glibc, which has 35 years of history and a reputation for being bloated, musl code is much cleaner. Study the bloat comparison table for musl and glibc. You will notice that glibc is associated with much bigger overhead and requires much more space because it supports legacy code and contains features not required by all software. For instance, locales supported by glibc are not a must-have for all applications and developers prefer using other, more performant libraries even when they are. musl, in turn, has the smallest static and dynamic overhead. It doesn’t support certain features such as legacy BSD behavior for setjmp/longjmp, legacy incorrect format specifiers, symbol versioning, lazy binding, etc. Instead, it provides some enhanced features, for example, lightweight headers, native UTF-8 multibyte, or correct behavior on end of file as per ISO C/POSIX requirements. As a result, musl is more secure due to a smaller attack surface and requires less space, but in retort, demonstrates inferior performance to glibc. Note that musl is compatible with most applications, but some of them require portability fixes and patch sets, which are referenced on musl’s compatibility page. BusyBox BusyBox, a set of command-line Unix utilities, was originally created for embedded operating systems, i.e., for devices with scarce resources. It comes as a single executable file, which means less overhead because of only one set of ELF headers. It contains smaller simplified versions of 400 common Unix utilities, thus providing a compact but complete environment for system maintenance. It is also customizable — commands and features can be added or removed. Some command-line options you require may be absent, but it is possible to install coreutils that includes numerous core utilities. The size of BusyBox is about 1 MB, so distributions based on this set of command line tools consume significantly less memory. OpenRC and apk tool Apart from musl and BusyBox, Alpine Linux uses other alternative tools. One of them is OpenRC, an init system which, in contrast to systemd utilized by most Linux distributions, is small, modular, more efficient on system resources, and isn’t bloated, i.e., doesn’t contain unnecessary features. Alpine also uses apk (Alpine Package Keeper) as a package manager. The apk-tools package is smaller than yum/rpm or deb/apt, and although it has drawbacks, it adds to Alpine size optimization. Benefits for Docker containers Small images The main advantage of Alpine Linux is its minuscule size. The smaller your Docker images are, the more you will save on cloud deployment regardless of scaling extent. Even if you add additional packages, the size of the Alpine-based image will still be several times smaller than with other popular distributions. You can try out Alpine Docker images of Liberica JDK and calculate how much you can save with our containers of only 42.72MB! Speed Another Alpine advantage is the pull speed. The development process implies constant modification to the code, with developers doing multiple push and pull requests per day. For instance, a base Alpine Docker image will be pulled x5 or x3 times faster than the Debian image, depending on the task. Rapid pull times save on traffic and increase the efficiency of team performance by reducing waiting time. Security The lesser the attack surface, the higher the security — Alpine Linux is as simple as can be. It doesn’t contain numerous packages or libraries, so the risk of exploits decreases. In addition, the project members implemented additional security measures: the binaries are compiled as Position Independent Executables, and OpenSSL was substituted with a more secure LibreSSL. The team also releases regular CVE fixes. Possible drawbacks Depending on your business goals, some Alpine Linux features may be suboptimal for you. Performance Although, in most cases, the difference between musl and glibc performance is insignificant (and some use cases like the performance of embedded systems are associated with better musl results), several benchmarks1,2 demonstrated inferior musl efficiency in the multi-threaded environment as compared to glibc. Sometimes the root cause lies in malloc implementation, and switching to mimalloc or jemalloc, for example, may solve the issue. However, depending on your application, you may find glibc-based distribution more suitable. No commercial support Alpine Linux is a community-based project as opposed to other popular Linux distros like SUSE or RHEL. Community distributions have their perks: overall economy, innovation-oriented philosophy, and informal atmosphere of forum-based support. In addition, enthusiasts working on free projects are no less skilled than engineers providing paid technical support. However, volunteers working on these projects are not obliged to react promptly to posted issues, nor do they have strict management or provide SLAs. If you Are used to business-like communication with providers Want your problems to be solved quickly Need timely patches and updates based on a strict schedule Alpine Linux may be unsuitable for you. We have already written about the importance of Linux support, so your situation may require a reliable business partner who will help you keep your OS safe and free of bugs. Challenging migration and compatibility issues If you already use a glibc-based distribution, the transition to musl will be anything but smooth. The reason is that some applications or their dependencies are dynamically compiled to libc, and as musl and glibc are different libc implementations, it will break the linker. You will have to recompile the whole application and its dependencies to solve compatibility issues. In some cases, an application will produce errors upon startup. For instance, you compiled a program on a glibc system. If you start it on a musl system, you will get a following result: /home/build # ./a.out /bin/sh: ./a.out: not found /home/build # ldd ./a.out /lib64/ld-linux-x86-64.so.2 (0x7fa126a75000) libc.so.6 => /lib64/ld-linux-x86-64.so.2 (0x7fa126a75000) /home/build # The list of libraries includes the dynamic resolver from libc, absent in musl. musl has ld-musl-x86_64.so.1 instead of ld-linux-x86-64.so.2. In addition, musl doesn’t support some DNS protocols: The size of UPD packets above 512 bytes via the Extension Mechanism for DNS (EDNS) DNS transport switching from UDP to TCP It may lead to issues with resolving DNS queries when using Alpine images. This is especially troublesome in Kubernetes clusters because of how Kubernetes handles name resolution. Alpine Linux versions 3.3 and earlier (to be fair, some glibc versions as well) may not work properly in K8s clusters, but DNS issues persist in later Alpine versions, too. Learn more about musl and glibc specificsAlpaquita Linux: like Alpine, but enterprise Inspired by Alpine team ambitions and success, BellSoft engineers have decided to use Alpine Linux as the foundation for the innovative solution we will offer to enterprise customers. It includes Alpaquita Linux, a new Linux distribution with all Alpine benefits plus Several libc implementations to choose from: improved musl (musl-perf developed by our engineers), standard musl, and glibc Enhanced APK tools Faster system boot Tools for Java development Ready containers with Java and CRaC (Coordinated Restore at Checkpoint) support Strict LTS release and updates schedule: six years of LTS support with two-year overlap with the previous LTS version, timely security updates, and security advisory 24/7 commercial support from engineers who develop the product Extensive documentation with install guides, how-to manuals, and techniques for efficient and secure containerization of Java applications. Download Alpaquita Linux for free What is more, Alpaquita Linux is only part of the deal! The solution called Alpaquita Cloud Native Platform also includes Liberica JDK Lite and Liberica Native Image Kit, so you will receive a complete technology stack packed in a microcontainer for developing and deploying cloud-native Java applications! Useful links Testing alternative C memory allocators in musl Rust benchmark results for musl - [Incorrect access to Java Collections causing ConcurrentModificationException](https://bell-sw.com/announcements/2022/08/05/incorrect-access-to-java-collections-causing-concurrentmodificationexception/): As part of the series of short articles by BellSoft, we look into various exceptions, their root causes, and elimination. ConcurrentModificationException is one of the most common issues related to collections. The solution comes down to understanding how to handle collections correctly. Problem Let’s examine the following example: import java.util.HashMap; import java.util.Map; public class AppMap { public static void main( String[] args ) { HashMap<String, String> map = new HashMap<>(); map.put("L", "London"); for (Map.Entry<String, String> entry: map.entrySet()) { if ("B".equals(entry.getKey())) { map.remove("B"); } } } } This code works fine, but what if you try to add more cities? map.put("L", "London"); map.put("B", "Berlin"); map.put("P", "Paris"); The app will fail on the second iteration with an Exception like this one: Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1493) at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1526) at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1524) at org.example.AppMap.main(AppMap.java:16) The code above is the most straightforward example leading to the ConcurrentModificationException. In practice, it may be hidden deep inside your business logic, and debugging could be quite hard. Root cause An attempt to modify a Collection while iterating through it is called a “concurrent modification.” Such modification makes no sense for most algorithms inside the standard Java collections. The name of the exception is quite misleading. You may think that “concurrent” modification assumes multithreading, concurrency, and parallelism. But as we’ve seen above, you can run into it even inside a single thread. This leads to a way more critical and frightening conclusion: it’s not the exception you should be afraid of. Unsupported modifications may cause the bugs that are very hard to detect. What’s the problem? Even though collections try to protect themselves, there’s no guarantee that these checks are perfect. For example, HashMap protects itself by counting modifications and failing with the ConcurrentModificationException if the values do not match. int mc = this.modCount; v = mappingFunction.apply(key); if (mc != this.modCount) { throw new ConcurrentModificationException(); } //... this.modCount = mc + 1; Here’s what HashMap documentation says: Extract from HashMap docs The iterators returned by all of this class’s “collection view methods’’ are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs. Iterators will attempt to detect concurrent modifications, but you can easily avoid these checks. Let’s replace HashMap with ArrayList in our example. Will it fail or not? What’s your take? import java.util.*; public class AppList { public static void main( String[] args ) { List<String> list = new ArrayList<>(Arrays.asList("L", "B", "P")); for (String entry: list) { if ("B".equals(entry)) { list.remove("B"); } } } } The answer is no. The exception is detected and thrown purely on a best-effort basis. Solution Debugging ConcurrentModificationException is hard. You must find all modification operations enhanced for loops (e.g., LinkedHashMap.get() modifies its collection), concurrent access to references to the collection, etc. The best thing you can do is to prevent the modifications by hiding collection references whenever possible. Hide your collection inside a private field, never return references to the collection from methods, and so on. To write concurrent code, you should master locking and synchronization. Java Concurrency in Practice (JCIP) is your best friend in this regard. Subscribe to our newsletter - [Liberica Native Image Kit 22.2.0 and 21.3.3 builds are out](https://bell-sw.com/announcements/2022/08/05/liberica-native-image-kit-22-2-0-and-21-3-3-builds-are-out/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 22.2.0 and 21.3.3 as part of Critical Patch Update (CPU) release cycle. The builds contain several security fixes and enhancements. Liberica Native Image Kit is a GraalVM-based tool that helps to convert JVM-based applications into native executables and thus minimizes resource consumption and accelerates the startup time of applications. Liberica NIK supports multiple system configurations and languages and is part of a Cloud Native Buildpack together with Spring, which enables seamless generation of native images with Spring Boot. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Summary of fixes and enhancements List of security issues fixed Summary of fixes in Liberica NIK Enhanced AWT/Swing support Set up your workspace Generate configuration files Create a native image for SwingSet2 app (Linux and macOS) Create a native image for SwingSet2 app (Windows) Conclusion Summary of fixes and enhancements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2022-34169 7.5 xml java.xml network low none none unchanged none high none CVE-2022-21540 5.3 core-libs java.base network low none none unchanged low none none CVE-2022-21541 5.9 core-libs java.base network high none none unchanged none high none CVE-2022-21549 5.3 core-libs java.base network low none none unchanged none low none Summary of fixes in Liberica NIK CVEs fixed in Liberica NIK per version: CVE CVSS 21.3.3 (JDK 11) 21.3.3 (JDK 17) 22.2.0 (JDK 11) 22.2.0 (JDK 17) CVE-2022-34169 7.5 ● ● ● ● CVE-2022-21540 5.3 ● ● ● ● CVE-2022-21541 5.9 ● ● ● ● CVE-2022-21549 5.3 - ● - ● Enhanced AWT/Swing support In the previous Liberica NIK release, we added support for AWT/Swing for Linux. Now we extend the support to Windows and macOS. In the case of GraalVM with JDK 11 or 17 on Windows, an application will be converted into a native executable plus dynamic libraries necessary for AWT/Swing and taken from the JDK. As far as macOS is concerned, an app will be transformed into a single native executable. Below you will find the detailed instructions on how to create native images of a SwingSet2 demo app using Liberica NIK for three platforms — Linux, macOS, and Windows. Set up your workspace First, download Liberica Native Image Kit for your platform. Refer to the Install Guide for installation instructions. Set the PATH variable to Liberica NIK: PATH=/bin:$PATH Download JDK 8 demos with SwingSet2. You will find the app in the demo/jfc/SwingSet2 folder upon archive extraction. Generate configuration files The next step is to create configuration files specific to the SwingSet2 demo using a native image agent. Run java from Native Image Kit with “-agentlib:native-image-agent=config-output-dir=conf-dir” parameters java -agentlib:native-image-agent=config-output-dir=conf-dir -jar SwingSet2.jar Click on several demo tabs and select several Look & Feels from “Look & Feel” menu Close the SwingSet2 demo The directory conf-dir contains configuration files in JSON format. The reflect-config.json file describes classes that will be accessed reflectively from the demo. It includes both the reflection classes related to the SwingSet2 demo and to JDK classes. Edit the reflect-config.json file to leave only the description for SwingSet2 demo classes accessed by reflection. You need to leave only the reflections related to the SwingSet2 demo. Below you will find the adjusted reflect-config.json file. reflect-config.json [ { "name":"ButtonDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"ColorChooserDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"ComboBoxDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"FileChooserDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"HtmlDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"ListDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"OptionPaneDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"ProgressBarDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"ScrollPaneDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"SliderDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"SplitPaneDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"TabbedPaneDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"TableDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"ToolTipDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] }, { "name":"TreeDemo", "methods":[{"name":"","parameterTypes":["SwingSet2"] }] } ] The resource-config.json file defines resources that should be accessible at image run time. It includes both resources related to SwingSet2 demo and for Swing Look&Feels. Edit the resource-config.json file to leave only the resources related to the SwingSet2 demo. Find the adjusted resource-config.json file below. resource-config.json { "resources":{ "includes":[ { "pattern":"\\Qresources/seaweed.html\\E" }, { "pattern":"\\Qresources/title.html\\E" }, { "pattern":"\\Qresources/ant.html\\E" }, { "pattern":"\\Qresources/bug.html\\E" }, { "pattern":"\\Qresources/preface.html\\E" }, { "pattern":"\\Qresources/king.html\\E" }, { "pattern":"\\Qresources/images/htmldemo/header.jpg\\E" }, { "pattern":"\\Qresources/images/htmldemo/forward.jpg\\E" }, { "pattern":"\\Qresources/images/htmldemo/back.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/book.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/ant.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/bug.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/bug2.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/king.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/micro.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/seaweed.jpg\\E" }, { "pattern":"\\Qresources/images/Octavo/crest.jpg\\E" }, { "pattern":"\\QButtonDemo.java\\E" }, { "pattern":"\\QColorChooserDemo.java\\E" }, { "pattern":"\\QComboBoxDemo.java\\E" }, { "pattern":"\\QFileChooserDemo.java\\E" }, { "pattern":"\\QHtmlDemo.java\\E" }, { "pattern":"\\QInternalFrameDemo.java\\E" }, { "pattern":"\\QListDemo.java\\E" }, { "pattern":"\\QOptionPaneDemo.java\\E" }, { "pattern":"\\QProgressBarDemo.java\\E" }, { "pattern":"\\QScrollPaneDemo.java\\E" }, { "pattern":"\\QSliderDemo.java\\E" }, { "pattern":"\\QSplitPaneDemo.java\\E" }, { "pattern":"\\QTabbedPaneDemo.java\\E" }, { "pattern":"\\QTableDemo.java\\E" }, { "pattern":"\\QToolTipDemo.java\\E" }, { "pattern":"\\QTreeDemo.java\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/apple.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/asparagus.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/banana.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/broccoli.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/cantaloupe.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/carrot.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/corn.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/grapefruit.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/grapes.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/kiwi.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/onion.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/peach.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/pear.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/pepper.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/pickle.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/pineapple.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/raspberry.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/strawberry.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/tomato.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/food/watermelon.jpg\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/cab.gif\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/cab_small.gif\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/fish.gif\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/fish_small.gif\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/moon.gif\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/moon_small.gif\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/sun.gif\\E" }, { "pattern":"\\Qresources/images/ImageClub/misc/sun_small.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b1.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b1d.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b1p.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b1r.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b2.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b2d.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b2p.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b2r.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b3.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b3d.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b3p.gif\\E" }, { "pattern":"\\Qresources/images/buttons/b3r.gif\\E" }, { "pattern":"\\Qresources/images/buttons/bl.gif\\E" }, { "pattern":"\\Qresources/images/buttons/bldn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/bm.gif\\E" }, { "pattern":"\\Qresources/images/buttons/bmdn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/br.gif\\E" }, { "pattern":"\\Qresources/images/buttons/brdn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/c.gif\\E" }, { "pattern":"\\Qresources/images/buttons/cb.gif\\E" }, { "pattern":"\\Qresources/images/buttons/cbr.gif\\E" }, { "pattern":"\\Qresources/images/buttons/cbrs.gif\\E" }, { "pattern":"\\Qresources/images/buttons/cbs.gif\\E" }, { "pattern":"\\Qresources/images/buttons/cdn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/ml.gif\\E" }, { "pattern":"\\Qresources/images/buttons/mldn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/mr.gif\\E" }, { "pattern":"\\Qresources/images/buttons/mrdn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/rb.gif\\E" }, { "pattern":"\\Qresources/images/buttons/rbp.gif\\E" }, { "pattern":"\\Qresources/images/buttons/rbr.gif\\E" }, { "pattern":"\\Qresources/images/buttons/rbrs.gif\\E" }, { "pattern":"\\Qresources/images/buttons/rbs.gif\\E" }, { "pattern":"\\Qresources/images/buttons/tl.gif\\E" }, { "pattern":"\\Qresources/images/buttons/tldn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/tm.gif\\E" }, { "pattern":"\\Qresources/images/buttons/tmdn.gif\\E" }, { "pattern":"\\Qresources/images/buttons/tr.gif\\E" }, { "pattern":"\\Qresources/images/buttons/trdn.gif\\E" }, { "pattern":"\\Qresources/images/combobox/brenteyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/brenthair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/brentmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/georgeseyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/georgeshair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/georgesmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/hanseyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/hanshair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/hansmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/howardeyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/howardhair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/howardmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jameseyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jameshair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jamesmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jeffeyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jeffhair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jeffmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/joneyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jonhair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/jonmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/laraeyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/larahair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/laramouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/larryeyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/larryhair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/larrymouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/lisaeyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/lisahair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/lisamouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/michaeleyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/michaelhair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/michaelmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/philipeyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/philiphair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/philipmouth.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/scotteyes.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/scotthair.jpg\\E" }, { "pattern":"\\Qresources/images/combobox/scottmouth.jpg\\E" }, { "pattern":"\\Qresources/images/filechooser/find.gif\\E" }, { "pattern":"\\Qresources/images/filechooser/gifIcon.gif\\E" }, { "pattern":"\\Qresources/images/filechooser/help.gif\\E" }, { "pattern":"\\Qresources/images/filechooser/jpgIcon.jpg\\E" }, { "pattern":"\\Qresources/images/list/blue.gif\\E" }, { "pattern":"\\Qresources/images/list/cyan.gif\\E" }, { "pattern":"\\Qresources/images/list/gray.gif\\E" }, { "pattern":"\\Qresources/images/list/green.gif\\E" }, { "pattern":"\\Qresources/images/list/magenta.gif\\E" }, { "pattern":"\\Qresources/images/list/red.gif\\E" }, { "pattern":"\\Qresources/images/list/yellow.gif\\E" }, { "pattern":"\\Qresources/images/optionpane/bottle.gif\\E" }, { "pattern":"\\Qresources/images/scrollpane/colheader.jpg\\E" }, { "pattern":"\\Qresources/images/scrollpane/crayons.jpg\\E" }, { "pattern":"\\Qresources/images/scrollpane/lowerleft.jpg\\E" }, { "pattern":"\\Qresources/images/scrollpane/rowheader.jpg\\E" }, { "pattern":"\\Qresources/images/scrollpane/upperleft.jpg\\E" }, { "pattern":"\\Qresources/images/scrollpane/upperright.jpg\\E" }, { "pattern":"\\Qresources/images/splitpane/earth.jpg\\E" }, { "pattern":"\\Qresources/images/splitpane/moon.jpg\\E" }, { "pattern":"\\Qresources/images/tabbedpane/blake.gif\\E" }, { "pattern":"\\Qresources/images/tabbedpane/brooke.gif\\E" }, { "pattern":"\\Qresources/images/tabbedpane/david.gif\\E" }, { "pattern":"\\Qresources/images/tabbedpane/ewan.gif\\E" }, { "pattern":"\\Qresources/images/tabbedpane/ewan.jpg\\E" }, { "pattern":"\\Qresources/images/tabbedpane/hania.jpg\\E" }, { "pattern":"\\Qresources/images/tabbedpane/laine.jpg\\E" }, { "pattern":"\\Qresources/images/tabbedpane/matthew.gif\\E" }, { "pattern":"\\Qresources/images/tabbedpane/stephen.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JButton.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JColorChooser.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JComboBox.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JDesktop.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JEditorPane.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JFileChooser.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JList.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JOptionPane.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JProgressBar.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JScrollPane.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JSlider.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JSplitPane.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JTabbedPane.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JTable.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/JTree.gif\\E" }, { "pattern":"\\Qresources/images/toolbar/ToolTip.gif\\E" }, { "pattern":"\\Qresources/images/tooltip/cow.gif\\E" }, { "pattern":"\\Qresources/index.html\\E" }, { "pattern":"\\Qresources/swingset.properties\\E" }, { "pattern":"\\Qresources/tree.txt\\E" } ]}, "bundles":[ { "name":"resources.swingset" } ] } 4. Set java.awt.headless Java property explicitly to false: -Djava.awt.headless=false Create a native image for SwingSet2 app (Linux and macOS) To build the native image, you need to provide the resource and reflection files apart from stating the java.awt.headless property. Run the following command native-image -Djava.awt.headless=false -H:ReflectionConfigurationFiles=conf-dir/reflect-config.json -H:ResourceConfigurationFiles=conf-dir/resource-config.json -jar SwingSet2.jar Metal Look&Feel (Linux) A note for macOS users — to run the application, go to the app directory and use the following command: ./SwingSet2 Aqua Look&Feel (macOS) Create a native image for SwingSet2 app (Windows) You need to have Microsoft Visual Studio installed to create a native image. To set the environment required for Microsoft Visual Studio, run the vcvars64.bat file: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat After that, provide the resource and reflection files. To enable HiDPI support, provide the manifest file “SwingSet2.exe.manifest”, where dpi1:dpiAware is set to true/PM dpi2:dpiAwareness is set to PerMonitorV2, PerMonitor, system Finally, the command for generating a native image is as follows: native-image.cmd -Djava.awt.headless=false -H:ReflectionConfigurationFiles=conf-dir/reflect-config.json -H:ResourceConfigurationFiles=conf-dir/resource-config.json -jar SwingSet2.jar As a result, the SwingSet2.exe and SwingSet2.exe.manifest will be produced. Windows Look&Feel (Windows) Conclusion BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. Not only does Liberica Native Image Kit enable you to create performant microcontainers with minimal startup time, it enhances the experience of working with Swing and AWT apps. Download the latest version of Liberica NIK now! Download Liberica NIK - [Enhance Linux security on server and cloud](https://bell-sw.com/announcements/2022/08/10/enhance-linux-security-on-server-and-cloud/): For a long time, Linux remained a niche operating system and thus enjoyed peace and quiet in a world filled to the brim with cybercrime. But today, about 80% of servers and 90% of cloud infrastructure run on Linux. The sky-rocketing popularity of this OS couldn’t escape the attention of hackers, and as a result, Almost 13 million malware attacks targeted Linux-based systems in 2021 Nearly 14 million Linux-based devices connected to the Internet had exposed ports in 2021 Approx. 1.7 million samples of new Linux malware were discovered in 2022, a 650% increase compared to 2021 Impressive? Frightening. Linux is not the problem — it is as secure by design as any other OS. But it is not enough to download and deploy a Linux distribution, which would be similar to fencing off the territory and leaving the backdoor open. A company should apply additional measures for security hardening. So read on and discover the best practices for strengthening your Linux-based system in server and cloud environments. Best practices for Linux server security User authentication and password policy Only necessary packages Regular updates Network protection SSH hardening Safety in the cloud Choosing a secure Linux distribution Security features Regular update cycle SELinux vs AppArmor Alpaquita Linux with accent on security Comparative table Best practices for Linux server security User authentication and password policy If this recommendation seems obvious to you, remember that 81% of data breaches are caused by weak passwords. Enforce the strong password policy at your company: a password should be a mix of at least ten characters, including digits, special characters, and letters in lower and upper cases. In addition, enable password expiration and “faillog” to set the limit of login attempts. To configure user authentication and password verification policies, use Linux PAM (Pluggable Authentication Modules). Numerous Linux-compatible password managers on the market provide additional password hardening functionalities such as two-factor authentication, vault auditioning, etc. Some are open-source (Bitwarden), and others require a subscription (1Password, Dashlane). The choice depends on your budget, requirements, and distro. Another good practice is to disable root via SSH and never use it. This prevents attackers from gaining access to the entire system. Only necessary packages Software packages tend to accumulate on systems like garbage. But the more utilities you install and preserve, the more vulnerabilities you bring into your system. So keep only the software required for a server, disable unnecessary services, and delete other components and features. First, get a list of all services and packages. Linux distros have different package managers, so the commands vary slightly: For RHEL/CentOS, run yum list installed In case of Debian/Ubuntu, run apt list --installed Alpine Linux uses apk (Alpine Package Keeper) tool, so the command is apk info After that, you can analyze the packages, remove unnecessary ones, and update the remaining. Note that instead of unsafe tools such as telnet, rlogin, rsh, etc. you should utilize their more secure alternatives: SSH, sftp, SCP, and so on. Regular updates Regular software updates are part of the security hardening procedure. Linux distribution providers, be it a community or a company, release regular security patches to the Linux kernel and other packages. These patches should be integrated as soon as possible after they come out to protect the server from exploits. It is possible to perform manual or automatic updates. You can update only the kernel, selected packages, or the whole system. For the sake of brevity, let us take yum as an example. To check whether there are available updates, run yum check-update To update all packages, run sudo yum update To update only selected packages, run sudo yum update In addition, yum enables the integration of security updates only without bug fixes: sudo yum update --security If you want to update only the kernel, run sudo yum update kernel and sudo reboot Some companies also offer kernel live patching to avoid system reboot and minimize downtime. An important step is to check your Linux distribution for end-of-life (EOL) when vendors stop supporting and releasing updates for an outdated OS version. The EOL schedule can be found on the vendor’s website. To minimize security risks, you should plan the migration to a newer version in advance. Note that your application runtime should also be regularly updated. Java boasts quarterly security releases in the form of Critical Patch Updates (CPU) with CVE fixes and other enhancements. Java updates can also be automated through Liberica Administration Center (LAC), a tool for monitoring and updating Java runtimes in the Windows PC fleet from a single dashboard. Find out more about LAC Network protection Network intrusions are widespread: In the US, they accounted for 32% of cyberattacks in 2022. To protect your network, close unused open TCP and UPD ports. Use the ss or netstat commands to list all open ports, for example: ss -tulpn | grep LISTEN or netstat -tulpn | grep LISTEN where -t stands for all TCP ports, -u for UPD ports, -l for listening server sockets, -p for the PID, -n for not resolving names. After closing the unwanted ports, use a firewalld or iptables tool to configure the firewall rules. Fail2ban is a valuable tool that prevents brute force attacks by scanning the log files and banning IPs associated with malicious activities. SSH hardening Secure Shell (SSH) is a reliable protocol, but should be configured with care. Use the nano /etc/ssh/sshd_config command to retrieve the configuration file and adjust several default settings. First thing first, enable the more secure v2 of the protocol by removing # before Protocol 2: # The default requires explicit activation of protocol 1 Protocol 2 Secondly, disable empty passwords by changing #PermitEmptyPasswords no to PermitEmptyPasswords no Thirdly, if you have servers with GUI, you might want to disable X11 forwarding that allows tunneling GUI applications to SSH. Change X11Forwarding yes to X11Forwarding no It is also possible to change the max. authentication attempts, which is set to six by default. Let’s set it to three, for example: MaxAuthTries 3 These are just a few basic SSH hardening tips. You can find more in the dedicated article. After you are done, save the file and restart SSH by running systemctl restart sshd Safety in the cloud Linux is the most popular OS for Docker containers, and LXC (Linux Containers) accounted for 33.5 percent market share of the containerized technologies in 2021, followed by Docker. Developers choose Linux for its flexibility and security, but as the recent Linux Thread Report shows, this OS is not invincible. So extra measures should be taken to protect your Linux-based containers. Firstly, choose a reliable Linux distribution. A more detailed overview of Linux distros by security is presented below, but as far as the cloud is concerned, the most frequently attacked OS (50.8%) is CentOS. The support for this distro was suspended in 2019, so it contains numerous vulnerabilities. Coupled with the fact that it is the most heavyweight Linux in the cloud, we highly recommend migrating to another distribution. In addition, watch what you put into your containers. It applies both to the OS version and open-source components. According to the report by Snyk, 56% of survey respondents experienced misconfiguration or incidents with known unpatched vulnerabilities in the cloud-native environment. So our advice is: never pull packages from untrusted sources and always check for the latest versions with vulnerability fixes. In most cases, containers run as root by default. The problem is that the root user in the containers is the same as on the host machine. Therefore, the compromised container enables the attacker to take control of the host easily. To avoid that, switch to rootless containers, which make it possible for an unprivileged user to create and manage containers without being a root. This method minimizes the attack surface and protects the host. Choosing a secure Linux distribution The most popular Linux distributions are relatively similar regarding security features integrated into the system. Linux patches are released whenever they become available. The main difference resides in the level of support, additional tooling, and configuration. Security features First, a distribution should be as clean as possible, i.e., contain only necessary packages. The rule of thumb is the fewer components there are, the smaller the attack surface. Instead of manually deleting packages as described above, installing an already concise distro would be better. Alpine Linux is the most convenient solution, with a base image size of 2.67MB. Not all distributions have commercial support and offer only community-based help (Debian, CentOS, Alpine Linux). Although a free distro helps to save money in the short term, for safety reasons, you should work with a vendor that delivers prompt fixes and emergency patches. A security advisory set up by the vendor is a bonus as it helps to stay aware of recently discovered CVEs. Some vendors also offer convenient tools for integrity checking, which are used to protect the system from unauthorized modifications. For instance, Advanced Intrusion Detection Environment (AIDE) installed as an additional package creates a database consisting of system files and monitors intrusions. Regular update cycle 2021 witnessed a record number of zero-day attacks that target zero-day vulnerabilities, which have been discovered but not yet patched. The longer a vendor delays patch updates, the higher the risk of exploits for the system. That’s why a Linux vendor shouldn’t delay releasing patches. Some vendors offer automatic updates and kernel live patching that helps to minimize downtime while keeping to security standards. In addition, LTS releases that receive security updates for an extended period are more beneficial for companies because they can migrate at their own pace. SELinux vs AppArmor Linux distributions also vary by the isolation technology — SELinux or AppArmor. Both are Mandatory Access Control (MAC) mechanisms aimed at restricting access of processes to files and isolating them from one another. SELinux controls access based on labels, whereas AppArmor uses file paths. SELinux provides a wide range of security policies but is very complex and challenging to use. On the other hand, AppArmor is more straightforward to deploy and use. In addition, SELinux is based on the “deny by default principle,” where all processes are blocked and require explicit permission for performing activities. AppArmor, in turn, enables the administrator to define restrictions from the start. SELinux is included with RHEL, CentOS, and Fedora, whereas Ubuntu, Debian, and SUSE Linux use AppArmor by default. Alpaquita Linux with accent on security BellSoft’s expertise in delivering the highest level of security to its customers has become the bedrock of Alpaquita Linux — a new Linux distribution, characterized by baked-in security features: Secure Boot, signed modules, kernel lockdown, userspace compilation options Security advisory Minimal attack surface with the base image size of only 2.9MB LTS versions and timely security updates 24/7 support from the engineers who develop the product Download Alpaquita Linux for free What is more, Alpaquita Linux is a perfect choice for a company working with Java as it contains tools for Java development. If you want to take your cloud apps to the next level, take a look at BellSoft’s Cloud Native Platform — a Linux container optimized for deploying Java microcontainers. Comparative table Below is a summary of popular Linux distributions according to their security features. CentOS RHEL Debian Ubuntu Alpine Linux Alpaquita Linux Commercial support x ✓ x ✓ x ✓ Security Advisory ✓ ✓ ✓ ✓ x ✓ MAC tool SELinux SELinux AppArmor AppArmor AppArmor AppArmor Base container image size (compressed) 79.65MB 10.3MB 29.93MB 27.01MB 2.67MB 2.9MB Comparing Linux distributions by security - [JavaFX guide: Go graphical with Java](https://bell-sw.com/blog/javafx-guide-go-graphical-with-java/): Java has a powerful solution for developing rich client applications — JavaFX. Introduced in 2008, it currently matures within the OpenJFX project delivering excellent cross-platform compatibility and modern out-of-the-box solutions for UI. With JavaFX, you can write familiar code in Java to develop desktop applications, without resorting to the complexities of JavaScript or .NET. In this article, we will explore JavaFX and its features and learn how to write, compile, and deploy JavaFX-based applications. Table of Contents What is JavaFX? Why use JavaFX? JavaFX vs Swing: key differences History and Evolution of JavaFX History Support roadmap End of Life for JavaFX support on Oracle Java SE 8 Key advantages of JavaFX How to install JavaFX JavaFX architecture and core components JavaFX components JavaFX APIs JavaFX tools and libraries Creating a user interface with JavaFX Scene Builder Layout Main UI components Create a JavaFX project Integrate FXML file in your JavaFX application Change the text of the Label using StringProperty How to switch scenes in JavaFX Animations in JavavFX Event handling in JavaFX How to compile and deploy JavaFX applications Conclusion What is JavaFX? JavaFX is an open-source platform containing graphics and media tools for developing and deploying rich client applications — programs that store and retrieve data and perform most operations locally, i.e., on a client machine. JavaFX apps run consistently in different environments: desktop, web, mobile, and embedded systems. They can also reference APIs from other Java libraries to use all capabilities of the language and connect to server apps. Why use JavaFX? Desktop applications are not a dying technology. There are numerous cases when local/native applications are a preferred choice for users as they provide a more advanced GUI experience, higher performance, and reliability. Use cases for desktop applications include: IDEs Editors Audio and video editors/players Games In addition, some web and mobile applications have their desktop counterparts, such as Skype, WhatsApp, Telegram, and Slack. So, desktop development is still a lucrative field for developers, which makes JavaFX a technology worth learning. As per TheirStack data, at least 1,087 companies whose data on the used software stack is available utilize JavaFX (as of February 2025). Furthermore, JavaFX goes beyond pure desktop development. For instance, JavaFX apps can be built as native images and used on mobile devices: Gluon offers tools for that purpose, and Liberica Native Image Kit can be used for the fast and convenient transformation of Java apps into native executables. JavaFX also goes well with embedded devices such as Raspberry Pi. Refer to our guide on running Liberica JDK with JavaFX on this popular single-board computer. JavaFX vs Swing: key differences You might be wondering at that point, “If a JDK already contains Swing, why should I bother with JavaFX?” As mentioned, JavaFX was designed as a substitution for Swing, which lacks many modern features. As a result, it offers many more opportunities for desktop development than Swing. Here is a short comparison of both technologies: JavaFX Swing Used for developing rich client applications with moden user interface Legacy library for GUI development Cleaner code base Many legacy features Integrated support for MVC Inconsistent support for MVC Evolves within the community that regularly introduces enhancements and new features No new functions are added Supports both CSS and code-based styling Only code-based styling Integrated API for concurrency No built-in API for concurrency FXML for declarative layout No support for declarative layout Built-in 3D graphics support Requires additional API for 3D Support for property binding No property binding Comes as a separate bundle starting with JDK 11 Included into JDK by default Comparative table for Swing and JavaFX Let’s compare two demo apps written with JavaFX and Swing, which illustrate button customization. Customizing a button with JavaFX: public class ScaleTransformationDemo extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { Button button = new Button(); button.setText("Click me!"); Scale scaleTransformation = new Scale(); scaleTransformation.setX(3.0); scaleTransformation.setY(2.0); scaleTransformation.setPivotX(0); scaleTransformation.setPivotY(0); button.getTransforms().add(scaleTransformation); VBox vbox = new VBox(button); Scene scene = new Scene(vbox); primaryStage.setScene(scene); primaryStage.setWidth(512); primaryStage.setHeight(256); primaryStage.show(); } } In the example above taken from the JavaFX Button tutorial, we added a scale transformation. The customization process with Swing is different. Customizing a button with Swing: public class ButtonCustomization extends BasicButtonUI { public static void main(String[] args) { JFrame f = new JFrame("Button UI Test"); f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); JPanel p = new JPanel(); p.setBackground(Color.white); f.setContentPane(p); p.setLayout(new FlowLayout(5, 5, 5)); p.setBorder(new EmptyBorder(10, 10, 10, 10)); final JButton button = new JButton("Click me!"); button.setFont(new Font("Calibri", Font.PLAIN, 14)); button.setBackground(new Color(0x2dce98)); button.setForeground(Color.white); button.setUI(new ButtonCustomization()); p.add(button); f.pack(); f.setLocation(500, 500); f.setVisible(true); } @Override public void installUI(JComponent c) { super.installUI(c); AbstractButton button = (AbstractButton) c; button.setOpaque(false); button.setBorder(new EmptyBorder(5, 15, 5, 15)); } @Override public void paint(Graphics g, JComponent c) { AbstractButton b = (AbstractButton) c; paintBackground(g, b, b.getModel().isPressed() ? 2 : 0); super.paint(g, c); } private void paintBackground(Graphics g, JComponent c, int yOffset) { Dimension size = c.getSize(); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(c.getBackground().darker()); g.fillRoundRect(0, yOffset, size.width, size.height - yOffset, 10, 10); g.setColor(c.getBackground()); g.fillRoundRect(0, yOffset, size.width, size.height + yOffset - 5, 10, 10); } } To avoid the complexity of code-based styling with Swing, try using CSS supported by JavaFX. Refer to the official JavaFX CSS Reference Guide for details. Now, if you want to animate the scaling of the JavaFX button, you simply need to use ScaleTransition. The same task won’t be as easy for Swing because it neither has a dedicated class nor concurrency support. History and Evolution of JavaFX History Graphical components have been present in Java since the early age of the language. At first, the focus was made on applets, small applications deployed in web browsers. But as the demand for full-blown desktop apps grew, Java evolved accordingly. First Java versions contained only AWT (Abstract Window Toolkit), a low-level abstract graphic API. Swing APIs introduced in 1998 were built on top of AWT and were aimed at extending the AWT functionality. JavaFX was released as part of the JDK in 2008. The first version of JavaFX was a scripting language built on top of the JVM. Version 2 of JavaFX already came as a set of Java libraries. Starting with Java 11, JavaFX was separated from JDK and now evolves within the OpenJFX project. Support roadmap Oracle Java 8 still includes JavaFX, which will be supported until March 2025. The release schedule of OpenJFX is based on quarterly updates with security patches and bug fixes. The community provides LTS and minor versions and commercial support by Gluon upon request. BellSoft bundles JavaFX with its own OpenJDK distribution, Liberica JDK (see below). JavaFX Release GA date Latest version Long-term support Java 11 September 2018 11.0.26 (January 2025) Yes Java 17 September 2021 17.0.14 (January 2025) Yes Java 21 September 2023 21.0.6 (January 2025) Yes Java 23 (current) September 2024 23.0.2 (January 2025) No Java 24 March 2025 (planned) Early access No Java 25 September 2025 (planned) Early access Yes JavaFX support roadmap JavaFX comes as a platform-specific SDK, jmods, or a set of artifacts. If you want to use the software with your Java runtime, download the necessary bundle from the developer’s website. We deliver a special flavor of Liberica JDK (Full version) containing LibericaFX — our instance of OpenJFX. Liberica for Embedded also comes with LibericaFX enabling the developers to create GUIs for embedded systems. JavaFX integrated into the runtime is very convenient: Minimize time and effort spent on installing JavaFX as a separate plugin; Get timely fixes together with runtime updates; Receive first-hand support from our engineers within the scope of JDK support. End of Life for JavaFX support on Oracle Java SE 8 Support for JavaFX on Java SE 8 ends in March 2025 as per Oracle Support Roadmap. It means that Oracle will no longer provide Oracle Java 8 builds with JavaFX. If you have been using Oracle Java SE 8 with JavaFX, you will have to either upgrade to a newer JDK version and request support for JavaFX or migrate to Liberica JDK 8, which enjoys extended long-term support until March 2032, including JavaFX support. You can get and use Liberica JDK 8 Full with Java FX at no cost. Or from SDKMAN: sdk install 8.0.432.fx-librca Key advantages of JavaFX There are several reasons to use JavaFX for developing GUI applications: JavaFX enables the developers to write clear, manageable code in Java, which is easy to update or debug. Writing code for desktop applications in JavaScript is far more complicated, and cross-platform compatibility of .NET apps is inconsistent. There is a short learning curve for Java developers or programmers who have already worked with Java-like technologies. JavaFX includes all necessary libraries for desktop development and supports CSS styling, FXML, and multithreading. It provides many functions out-of-the-box (see the section below), and as a bonus, it has a convenient JavaFX Scene Builder, a visual layout tool for designing UIs without coding. JavaFX is open source and part of an active community. Updates with security patches and bug fixes come out regularly, and even when Oracle ceases JavaFX support as part of Java 8 in 2025, the technology will still be developed by the community within the OpenJFX project. Thanks to the support for native image technology in Liberica NIK 21.3 and up, the JavaFX app can be bundled and deployed as native executables that take less space on a disk and start up almost instantly. How to install JavaFX As JavaFX evolves as a standalone project, there are two ways of getting it: you can download JavaFX separately from the OpenJFX website or get JDK builds bundled with JavaFX. BellSoft is one of few OpenJDK vendors that provide JDK binaries with JavaFX. You can get Liberica JDK Full version, which is a version with JavaFX, from Liberica JDK Download Center, or get Liberica JDK with JavaFX via a package manager. For instance, in SDKMAN they have an 'fx' tag. So, for instance, to get Liberica JDK 23 with JavaFX, run sdk install 23.fx-librca JavaFX architecture and core components JavaFX components JavaFX has an extensive range of features for developing GUIs: tables, buttons, trees, menus, and many more. It also supports CSS, 2D and 3D Graphics, and WebView. Key JavaFX components: Stage is a window in a JavaFX application. When the application starts, it creates a root Stage object, which is the main window of the app. But you can create multiple stages if your app has multiple windows. Scene is a component where you add GUI components. A Scene object is set on the Stage. Node is a superclass of all components added to the Scene Graph. Stage and Scene make the Scene Graph visible, but only Node elements added to the Scene are considered part of the Stage Graph. FXML is an XML-based markup language for creating a layout of JavaFX apps. FXML helps you to separate the layout code from the application code, making the latter more concise. JavaFX features with examples of subcomponents for better understanding are listed below: Core: FXML, Stage, Scene, etc. Layout: HBox, VBox, Border Pane, Text Flow, Flow Panel, etc. UI controls: Label, Button, TextField, MenuBar, etc. Container controls: Accordion, TablePane, etc. Web: WebView, etc. Charts: PieChart, BarChart, etc. Other tools: fonts, animation, effects, etc. JavaFX APIs JavaFX public APIs include all classes required for building a GUI application. The most important ones are listed below: javafx.scene provides a set of core classes for the JavaFX Scene Graph API, which is the foundation for JavaFX applications. There are several subpackages with classes for different purposes such as working with texts, images, media, keyboard and mouse input events handling, and many more. javafx.animation includes classes for adding and handling transition-based animations. javafx.css provides classes for working with styles using CSS. javafx.fxml includes classes for loading objects from the markup file. javafx.event provides classes for handling JavaFX events. javafx.geometry contains classes for working with 2D objects. javafx.stage provides container classes for the JavaFX content. JavaFX tools and libraries Numerous libraries, frameworks, and third-party resources enhance the experience of working with JavaFX. The libraries enable the developers to create beautiful apps with more concise code, and frameworks add extra functionality. Let’s discuss some of them briefly to illustrate the capabilities of the JavaFX platform you can make use of. MigLayout is an open-source library that aids in developing and managing layouts. It provides tools for multiple layout types: flowing, grip-based, docking, etc. The code written in MigLayout is very concise and represents the appearance of a layout clearly. Ikonli is a library providing numerous icon packs for different icons and making it possible to customize and style icons. RichTextFX offers tools for creating rich text editors and code editors with syntax highlighting and various fonts. JacpFX is a framework that helps to structure the app with loosely coupled, reusable JavaFX components. The task execution can be separated from UI changes in the client application, thus enabling the developers to avoid the multithreading issues. JavaFX functionalities include message-bus communication between components and asynchronous processes support. Skija provides Java bindings for Skia, an open-source library for developing rich 2D graphics. It is highly performant, easy to use, and robust, with support for color spaces, modern typography and GPU backends, and highly-optimized GPU rendering. Creating a user interface with JavaFX Scene Builder The easiest way to start building a UI with JavaFX is to use a Scene Builder. It has a convenient drag-and-drop interface where you can create a layout for your application and export it as an FXML file. Download, install, and open the application. Scene Builder interface On the left, you can see the Library with the groups of UI components: containers with panes and boxes, controls with buttons, text, labels, etc, and so on. In the middle on the left, there is a Document section showing the hierarchy of elements we will add to the Scene Graph, and at the bottom, there is a Controller where you can specify the Java Controller class for this window. On the right, there is the Inspector where you can customize each element. Layout Let’s start with a layout. Some important containers: HBox and VBox are the components that place all their child nodes in a horizontal or a vertical row accordingly. BorderPane is a pane that arranges the child nodes in the top, left, right, bottom and center positions. GridPane places the child nodes in a grid. FlowPane lays out the child nodes vertically or horizontally and can wrap them at their width or height accordingly. TilePane lays out the child nodes in a grid with equally sized cells. Let’s use BorderPane. Choose the component in the Containers section and drag it to the center of the canvas. This is going to be our starting point. You can customize the element. On the right, there are three sections: Properties, Layout, and Code. We will change the background color. Select Properties, Style, and paste -fx-background-color to the left cell and #E0F6FD to the right. Also, go to Layout and set Padding on the bottom to 10. Main UI components There are numerous UI components you can add to a JavaFX application, including: Text elements: Label, TextArea, TextField; Buttons: Button, CheckBox, RadioButton; Charts and diagrams: PieChart, BarChart; Sliders and bars: MenuBar, ScrollBar, Slider. In Controls, choose TextArea and drag it to the top of the BorderPane. Go to Code and enter text in fx:id. Choose Label in Controls and drag it to the center of the pane. Go to Code and enter dataLabel in fx:id. Also in Controls, choose Button and drag it to the bottom of the pane. Go to Properties and name the button Say hello! You can play around with colors, fonts, etc. After that, go to Code and enter helloButton in fx:id and sayHello in onAction. These fields are required to tie the FXML file to the code of our application. This is the resulting window. Finally, in the Controller section at the bottom, enter the name of the Controller you will use in your app. In my case, it was dev.cat.HelloController. Save the file and let’s move on to the IDE. Create a JavaFX project The easiest way to create a JavaFX project is to install an OpenJDK distribution bundled with JavaFX. In this case, you can create a project without adding additional dependencies for FX. Download Liberica JDK Full, which includes LibericaFX, an instance of OpenJFX. You can also get the binaries from your favorite package manager. For instance, with SDKMAN: sdk install 23.fx-librca Install Liberica JDK, then create a new Java project in your IDE. Creating JavaFX project Integrate FXML file in your JavaFX application Place the .fxml file into the resources directory. First of all, we need to make our Main application class extend Application from the javafx.application package, implement its method start(), and call the method launch() from the main method: public class Main extends Application { @Override public void start(Stage primaryStage) throws Exception { } public static void main(String[] args) { launch(args); } } Now, we need to load the FXML file in the start() method. For that purpose, create an instance of FXMLLoader and set the location of the FXML file using its setLocation() method. FXMLLoader loader = new FXMLLoader(); loader.setLocation(new URI("file:src/main/resources/home.fxml").toURL()); After that, create a root node and load the file: BorderPane pane = loader.load(); Finally, create a Scene, passing the root node as the parameter, and set the Scene on the Stage. The whole body of the method should look like this: @Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader = new FXMLLoader(); loader.setLocation(new URL("file:src/main/resources/home.fxml")); BorderPane pane = loader.load(); Scene scene = new Scene(pane); primaryStage.setScene(scene); primaryStage.show(); } It’s time to connect the FXML file to the Controller. Create a new Java class, for instance, HelloController. FXML files can be connected to Java fields and methods using the @FXML annotation. So, as we tied the button to the sayHello method, let’s create it in the Controller and annotate this method with @FXML. In addition, we need two fields, TextArea and Label, also annotated with @FXML. Make sure that the names of the variables are the same as fx:id field in the FXML file. public class HelloController implements Initializable { @FXML private TextArea text; @FXML private Label dataLabel; @FXML void sayHello() { } @Override public void initialize(URL location, ResourceBundle resources) { } Specify the name of the Controller in the FXML file if you haven’t done it in Scene Builder: The FXML file is now tied to our application. But the app doesn’t do anything useful yet, let’s fix that. Change the text of the Label using StringProperty It is possible to set a text or even an image of a Label that will be displayed when you run the application. For our simple scenario, we could simply use the setText() method of a Label class. However, we will implement another approach using the StringProperty, whose values can be observed or changed and displayed dynamically based on the user input. For that purpose, we will need to add and initialize the StringProperty field in the Controller: StringProperty name = new SimpleStringProperty(); In the initialize() method, we need to bind the value of Label to StringProperty: @Override public void initialize(URL location, ResourceBundle resources) { dataLabel.textProperty().bind(name); } Finally, in the sayHello() method, use the setMethod() of StringProperty to change the value of name to user input that we can extract from TextArea with getText(): @FXML void sayHello() { name.setValue("Hello, " + text.getText() + "!"); } The whole body of the Controller class: public class HelloController implements Initializable { @FXML private TextArea text; @FXML private Label dataLabel; StringProperty name = new SimpleStringProperty(); @FXML void sayHello() { name.setValue("Hello, " + text.getText() + "!"); } @Override public void initialize(URL location, ResourceBundle resources) { dataLabel.textProperty().bind(name); } You can now run the application and check that everything works correctly. How to switch scenes in JavaFX Our tiny application works with one scene, but in some cases, it won’t be enough. So, how do we switch scenes? First of all, you need to create a new FXML file and add it to the project. Then, you create a Controller class for this file and connect the business logic to the interface like we did before. There are several ways of switching scenes. Here’s one of them for the scenario when you need to change the scene as soon as the user pushes the button. First, create a static Stage field in the Main class and tie it to the primaryStage in the start() method: private static Stage stage; @Override public void start(Stage primaryStage) throws Exception { stage = primaryStage; FXMLLoader loader = new FXMLLoader(); loader.setLocation(new URI("file:src/main/resources/home.fxml").toURL()); BorderPane pane = loader.load(); Scene scene = new Scene(pane); primaryStage.setScene(scene); primaryStage.show(); } Second, create a static method switchToSecondScene() in the Main class. You can use any sensible name for the method, this one here is just an example. In this method, create an instance of FXMLLoader and set the location of the second FXML file. Then, create a new Scene and add it to the Stage: public static void switchToSecondScene() { FXMLLoader loader = new FXMLLoader(); loader.setLocation(new URI("file:src/main/resources/second-view.fxml").toURL()); BorderPane pane = loader.load(); Scene sceneTwo = new Scene(pane); stage.setScene(sceneTwo); stage.show(); } Finally, call the switchToSecondScene() method in the Controller connected to the first FXML file. If the scene changes upon button action, add the call to the button handling method: @FXML void switchScene() throws Exception { Main.switchToSecondScene(); } You now have two scenes in your JavaFX application, but what if you need to pass data between two Controllers? You can accomplish this task in four steps: Add a parameter to the switchToSecondScene() method that specifies the data you want to pass. For instance, a String: public static void switchToSecondScene(String text) { } Extract the data in the first Controller and pass it to the method: @FXML private TextArea text; @FXML void switchScene() throws Exception { Main.switchToSecondScene(text.getText()); } Define a method for saving the data in the second Controller: public void saveData(String text) { //saving logic } Get an instance of the second Controller in the switchToSecondScene() method by calling loader.getController() and call the Controller method for saving the data: public static void switchToSecondScene(String text) { FXMLLoader loader = new FXMLLoader(); loader.setLocation(new URI("file:src/main/resources/second-view.fxml").toURL()); BorderPane pane = loader.load(); SecondController controller = loader.getController(); controller.saveData(text); Scene sceneTwo = new Scene(pane); stage.setScene(sceneTwo); stage.show(); } Animations in JavavFX Animation is the process of changing the position of objects or applying transformations to create an illusion of movement. We can add animations to JavaFX applications. The javafx.animation package includes classes for applying various transitions to the objects. These transitions can be roughly classified as Simple transitions represented by FadeTransition, ScaleTransition, RotateTransition: The FadeTransition class is used to change the opacity of the JavaFX node over time. ScaleTransition enables scaling a node over time. RotateTransition enables rotating a node by a specified axis over time. Complex transitions represented by SequentialTransition and ParallelTransition: SequentialTransition plays a list of animations in sequential order. ParallelTransition plays a list of animations in parallel. Animation by trajectory is made possible with PathTransition, which moves a node over specified path over time. Custom transitions can be created using the Timeline class. You can add KeyFrames to the timeline specifying the animation and its duration. Take a look at an example of animating an object in JavaFX. Let’s return to our minimalistic application we created above. Suppose we want to animate a Button and add ScaleTransition to enlarge the button on mouse hover. Add the button field and annotate it with @FXML. In the initialize() method, create a ScaleTransition instance: public class HelloController implements Initializable { @FXML private Button helloButton; @Override public void initialize(URL location, ResourceBundle resources) { ScaleTransition scale = new ScaleTransition(Duration.seconds(1), helloButton); scale.setByX(0.5); scale.setByY(0.5); scale.setCycleCount(2); scale.setAutoReverse(true); helloButton.setOnMouseEntered(e -> scale.play()); } The code in the initialize() method Creates a new ScaleTransition, sets the duration and the object we want to apply this transition to; Sets the new size of a button with setByX() and setByY(); Returns the button to the normal size after playing the animation with setAutoReverse(true); Defines when the animation will be played, in our case, on mouse hover with setOnMouseEntered(). Event handling in JavaFX The application needs to react to the user's actions. In JavaFX, when the user interacts with the application nodes, for instance, pushes a button, an event is triggered. This event is handled by the application code. The javafx.event package includes classes that can be used to handle various events. Some of the key JavaFX events are: MouseEvent (MouseClicked, MouseEntered, MouseExited) is an event triggered when the mouse is used. KeyEvent (KeyPressed, KeyReleased, KeyTyped) is an event triggered when the keystroke is detected on a node. WindowEvent is an event triggered when window-related actions happen like showing or hiding a window. DragEvent is an event triggered when the user drags and drops items in the interface. In the example above, when we added action to our button, we used an event onMouseEntered meaning that when the user hovers a mouse over a button, an animation is played. helloButton.setOnMouseEntered(e -> scale.play()); You can also use the EventFilter when you need to evaluate the event before it is processed. For instance, we can disable exiting the application with the ESCAPE key: scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { if (event.getCode() == KeyCode.ESCAPE) { System.out.println("Escape key disabled"); event.consume(); } }); You can also create custom events by extending the Event class: public class CustomEvent extends Event { public CustomEvent(EventType eventType) { super(eventType); } //business logic here } How to compile and deploy JavaFX applications There are two ways of building JavaFX applications. You can create a standard JAR file or a native image. To build a JAR file, add the following plugin to your pom.xml file: org.apache.maven.plugins maven-jar-plugin dev.cat.LotteryApp You may also have to specify the start class in the properties section: 23 23 UTF-8 com.fxdemo.Main After that, you can create a JAR file with mvn clean package JavaFX applications can also be turned into native images that start up almost instantly. But these builds are OS and platform-specific, meaning that if you compile the app on AArch64 mac, it won’t run on x86_64 Linux. The solution is to build the native image further down the CI/CD pipeline, for instance, using GitHub Actions. Refer to these guides on building JavaFX native images and using GraalVM Native Image with GitHub Actions for more information. Conclusion JavaFX is still commonly used in enterprise development. This technology is actively developed by the OpenJFX community, and enterprise support is provided by several companies, including BellSoft. Starting with JavaFX is no challenge, and FXML integration makes desktop development with Java even more convenient. For more guides on JavaFX, refer to the official OpenJFX documentation. - [GraalVM 22.2.0 was released](https://bell-sw.com/announcements/2022/08/19/graalvm-22-2-0-was-released/): A new version of GraalVM 22.2.0 released on July 26 has many new features and enhancements in stock! You can find the complete list of updates in the release notes, and we will review the most important ones, boosting the technology efficiency and making native image generation even more convenient. Key improvements The size of the base binary was reduced by 42%. For instance, GraalVM CE shrank to 251 MB from 431 MB. This was achieved by separating the JavaScript, LLVM, and VisualVM from the main package: developers can add them if necessary Apple Silicon support was added to GraalVM Enterprise as an experimental feature and enhanced in GraalVM Community The compatibility of GraalVM Native Image and 3d party libraries was improved. Now, developers can use the GraalVM Reachability Metadata Repository to provide metadata for libraries unreachable to the Native Image during the build process The memory footprint was reduced. Many applications now need only 2 GB of Java heap to build a native executable GraalVM CE now supports heap dump at run time. In addition, developers can dump heaps of native executables with a new option -XX:+DumpHeapAndExit The GraalVM CE JIT-Compiler now uses less memory by releasing the unused memory in a stable state Liberica NIK with GraalVM open source at the core Liberica Native Image Kit (NIK) builds based on the latest GraalVM version are already out! Liberica NIK brings you all the benefits of GraalVM plus AWT/Swing support for Linux, macOS, and Windows JavaFX support musl support Latest versions of Liberica JDK 11&17 with security patches and bug fixes Support from a major OpenJDK contributor In addition, Liberica NIK is the default native-image compiler in Spring Native, a framework that supports the compilation of Spring applications into native executables. Try out Liberica NIK now and see how you can enhance your applications with native image technology! Download Liberica NIK - [Turning JavaFX apps into native images](https://bell-sw.com/announcements/2022/08/24/turning-javafx-apps-into-native-images/): In our recent guide to JavaFX, we summarized the key features of this platform and compared it to the most prominent competitors. If you are still wondering whether JavaFX is worth learning, refer to the guide. For those of you who have already mastered the software, we suggest going a step further and integrate it with native image technology. So let’s get our hands dirty and turn a JavaFX application into a native executable using Liberica Native Image Kit! What is Liberica Native Image Kit? Turn a JavaFX app into a native image! Enable native compilation Compile the application Speed and memory comparison Limitations Conclusion What is Liberica Native Image Kit? Liberica Native Image Kit (NIK) is a utility based on the GraalVM Community Edition. It helps to convert JVM-based applications into native executables. Liberica NIK enhances the experience of writing desktop applications as the native image takes up less disc space and starts up almost instantly. The benefits of using Liberica NIK in your enterprise development: Supports a wide variety of platforms, features, and languages. We support Windows, Linux, and MacOS, including ARM-based Macs. Starting with version 21.3, Liberica NIK works with AWT/Swing and OpenJFX Always on the latest versions of GraalVM CE and Liberica JDK with eliminated CVEs and bug fixes Receives support from the engineers who develop the product Turn a JavaFX app into a native image! Enable native compilation First, you need to download and install Liberica NIK. Go to the Liberica NIK Download Center and choose NIK version 22 for Java 11 or 17. You need the Full version as it includes LibericaFX, an instance of OpenJFX. Download the package and follow the instructions to install the utility on your platform. macOS users should get the .dmg package to avoid any issues with running Liberica NIK. You are now ready to compile Java applications natively. You have several options: Manual invocation. From your NIK installation, run native-image -jar Configure Maven build as described here Configure Gradle build as described here In all three cases, your application is likely to use resources such as icons or images. These resources must be made known to native-image so that it includes them in the resulting executable. There are two options to include and exclude resource identifiers, and you can use wildcards there:-H:IncludeResources='com.fxapp.resources.images.*'-H:ExcludeResources='com.fxapp.resources.unused.*' Alternatively, resources can be configured using JSON files. Many applications make use of dynamic language features such as reflective or JNI access. In this case, the feature being used, such as the name of the method called, is not known at image build time, so NIK cannot figure out exactly which method is called. These dynamic feature usages must be configured beforehand using JSON files as described here. You can create JSON files using the native image agent. Run your application with the agent enabled, probably several times to exercise different code paths. The agent captures dynamic feature accesses and creates configuration files automatically. These files can be passed to native-image without further editing. Compile the application Let us now compile a JavaFX app natively. We will use the BrickBreaker game as an example. You can utilize your own app or select a JavaFX demo on GitHub. BrickBreaker requires no dynamic feature configuration. At the same time, it uses lots of images that need to be included in the resulting executable. In this example, image names are passed using -H:IncludeResources with a regular expression: $ native-image \ -H:IncludeResources='ensemble.samples.shared-resources.brickImages.*' \ -jar BrickBreaker.jar After the compilation has finished, run $ ./BrickBreaker Natively compiled BrickBreaker demo Speed and memory comparison The figures below were obtained on an Intel Core i7 laptop running Ubuntu Linux. Liberica JRE 17.0.4.1 Native Image made with Liberica NIK 22.2.0-JDK17 Startup time 710ms 366ms Maximum RSS 176M 133M Executable size 276M (JRE+jar) 36M Limitations Modules javafx.media and javafx.web are currently not supported for native compilation. The javafx.media module is responsible for adding the playback of media and audio content. The javafx.web module defines the WebView functionality. Conclusion In this step-by-step tutorial, we learned how to generate JavaFX native images. Liberica NIK can also be used to create tiny containers saving precious cloud resources. Give it a try! Download Liberica NIK - [Avoiding side effects of Java containerization](https://bell-sw.com/announcements/2022/09/01/avoiding-side-effects-of-containerization/): Docker, Kubernetes, and other technologies for containerized applications are the must of modern development, especially in the case of microservices. But containerization is an intricate science, and even seasoned developers can make mistakes when working with containers. In this article, you will find out how to avoid these pitfalls and solve the issues you might have already encountered. Table of Contents Overview Be cautious about automatic Docker image generation Set the -Xmx parameter correctly Avoid automatic SerialGC switching Use newer OpenJDK distributions Set memory limits for the container How to enable Native Memory Tracking How to disable Swap Memory Choose a small base image Conclusion Overview The process of containerization seems easy — pack the application in a container, deploy it to the cloud, done. But later you run against decreased performance and increased cloud bills: What could go wrong? There are typical mistakes in Java development leading to deteriorated performance, including Faulty application logic; Incorrect usage of databases; Concurrency issues; RAM under- or overutilization; Improper server infrastructure. In the case of containers, developers should focus on memory usage and infrastructure. Below you will find the recommendations on how to customize the containers and tune JVM settings to reach optimal performance and footprint indicators. But before we go any further, it is worth noting that no JVM fine-tuning will help eliminate strategic errors. Just like local optimization in algorithms cannot beat asymptotics, container tuning won’t fix the code with memory leaks or unnecessary calculations. So don’t neglect application profiling to prevent or remedy such errors on time. Be cautious about automatic Docker image generation Imagine you already have a well-written Java application and want to spend minimum time and effort to containerize it and deploy into production. There already exist solutions in the Java world that help with automatic container generation, for example, Paketo Buildpacks. Simply install the pack utility and then follow the official guidelines on developing applications with this tool. Alternatively, check our guide on utilizing buildpacks for Java workloads. Paketo Buildpacks use Liberica JDK, a Java runtime recommended by Spring, by default. It provides JDK at build time and JRE at runtime, so the resulting container will be smaller. But despite the fact that buildpacks accelerate and facilitate development, neglecting the JVM tuning will lead to deteriorated performance. For instance, if you run the application built automatically, you may notice that it is using less CPU and cores than allocated in the container. For instance, the container can work with 4 GB of memory and 16 processors, but the JVM has access only to 1 GB and 2 threads. This particular issue was solved in the latest buildpack versions. It can also be resolved by setting the container parameters manually and turning off automatic core count and memory calculator with docker run --rm --tty --publish 8080:8080 --env 'JAVA_TOOL_OPTIONS=-Xmx3072m -XX:ActiveProcessorCount=4' --memory=4G paketo-demo-app We recommend always checking and setting these parameters correctly. Automatic image generation saves time, but as a result you get a black box that may not fit your purposes. Luckily, there are numerous configuration options you can implement to tune the JVM at runtime. The full list of flags can be found on the Liberica Buildpack page. Set the -Xmx parameter correctly If you want to create the image of your application manually, you need to set the -Xmx parameter correctly. It is used to define the max. heap size, but to state the right number you should first define how much RAM the application uses. The idea is to set the RAM limit for the container higher than the JVM heap size. In addition, the server should have enough RAM to start all your containers. If the -Xmx value is less than the application needs, it will crash with the OutOfMemoryError: Java heap space error. How to determine the amount of memory your application requires? The easiest method is to activate the GC logging in JVM parameters by running -verbose:gc -XX:+PrintGCDetails This way, before the application exits, the total and used memory values will be printed in the console. The amount of memory allocated through -Xmx should be bigger than the de facto used memory found in the GC logs. The difference depends on the peak values. For that purpose, perform load testing, run the application with a profiler, for instance, Java Flight Recorder, and analyze how RAM consumption depends on the load. Avoid automatic SerialGC switching Java has a remarkable feature: if you limit your application to 2 GB RAM and less than 2 processors, SerialGC will switch automatically. SerialGC is the oldest and relatively efficient garbage collector perfect for single-thread applications. But at the same time, it is the slowest Java collector. The conditions of SerialGC activation are detailed in the select_gc_ergonomically() and is_server_class_machine() functions in OpenJDK documentation. Note that flags such as -XX:+UseG1GC won’t help if you allocate too little memory. To sum up, even if you want to save memory, do not set less than 2 GB in the -Xmx parameter. To further enhance garbage collection, choose a GC implementation best fit for your workloads. Use newer OpenJDK distributions The OpenJDK community has been working actively on container support. A lot of issues have been eliminated. For example, there is a known issue: top and free tools inside of a container show the total host memory, not the one you assigned in the -memory parameter. You can check it by running the container in the interactive mode: docker run \ --interactive \ --tty \ --memory 10m \ bellsoft/liberica-openjdk-alpine After that, you can run commands such as free -m or top bn1 in the console. No matter how you experiment with startup flags, you won’t see the desired 10 MB. The console will print only the total amount of memory on your computer. Earlier, JVM demonstrated the same behavior. Run this command in the console: docker run -m 1gb openjdk:8u131 java -XshowSettings:vm -version Now compare it to the latest OpenJDK version: docker run -m 1gb liberica-openjdk-alpine:latest java -XshowSettings:vm -version When using the older version, the console output will be the total host memory just like with free. In the case of the newer version, the value will be equal to about a quarter of a claimed memory limit. The same goes for processor count: Runtime.getRuntime().availableProcessors() It is the result of introducing the -XX:+UseContainerSupport flag into OpenJDK 10 and further, where it is activated by default. You can experiment with this flag. Switch it off, and you will get the previous behavior even with the newer OpenJDK versions: docker run -m 1gb liberica-openjdk-alpine:latest java -XX:-UseContainerSupport -XshowSettings:vm -version It means that containers are accounted for in the latest versions. Therefore, you should always update your distribution to the latest version to prevent similar issues. You don’t have to migrate straight to JDK 17, the latest LTS release, although it is desirable because this version contains a lot of new features. If you work with Java 8, this problem was fixed in 8u212. But the best practice is to use the newest official OpenJDK release for your Java version. Liberica JDK updates always come out on time guaranteeing that your runtime will be free of known bugs and vulnerabilities. Set memory limits for the container Containers also have memory limitations. For example, you can set the memory limit in Docker with the -memory flag. Kubernetes has a more complex system: you have to specify request (searches for the most suitable server) and limit (sets strict memory limits for cgroups) separately. In any case, it makes sense to allocate more memory to the container than the app requires, but there are certain intricacies we will discuss below. How to enable Native Memory Tracking GC logs don’t show all the memory used by your application. For example, extra resources may be spent when utilizing Off-heap memory (ByteBuffer.allocateDirect); External native libraries loaded through System.loadLibrary; Significant heap fragmentation due to fact that malloc allocates memory in blocks; and so on. When determining container memory limits, you should consider not only- Xmx, but also total memory. You can check it by using Native Memory Tracking. First, start your Java application with the following flag: -XX:NativeMemoryTracking=detail Then, to see the total memory with its internal part, run jcmd $pid VM.native_memory where $pid is the Java process identifier. It can be found by running jcmd without parameters or using ps aux | grep java. You can track memory changes by using jcmd $pid VM.native_memory detail.diff How to disable Swap Memory Suppose you already know the precise amount of memory required by your application. It is possible to set the -Xmx value higher than available memory on the server or in the container, but the performance will deteriorate. The reason is that heaps in Java differ from those in C/C++. Scripts and native programs in C/C++ efficiently make it into the swap. In Java, we have to work with one heap, where Java objects are evenly distributed over the whole address space of the process. If most of them get into the swap file, the application will slow down significantly. What should we do to avoid that? Set the correct -Xmx value or even disable swap in the host machine’s OS and the container. Use a swapoff -a command for host and delete the respective entries from the /etc/fstab file (sed -i '/ swap / s/^/#/' /etc/fstab). In Docker, set the --memory-swap parameter equal to --memory. Fortunately, Kubelet will not work with enabled swap memory by default if you didn’t allow it explicitly with KUBELET_EXTRA_ARGS=--fail-swap-on=false and the memorySwap parameter in KubeletConfiguration. Choose a small base image One more important factor to consider is the size of the resulting container. If you stuff it with unnecessary files, OS packages, dependencies, JDK instead of JRE, and a heavy base OS image, you will soon notice how the bills for cloud storage start to inflate. We prepared a detailed guide on trimming your Docker container images of Java applications, which will help you master useful Docker commands and Java tools to reduce the size of Java containers. But in many cases, implementing the right OS enables the developers to cut the size of the container image instantly and without other adjustments. The question is, which OS to choose? CentOS is no longer supported and filled with vulnerabilities, RHEL doesn’t have a free version, Alpine Linux, albeit small, lacks commercial support. Seeing the demand for small, secure, and supported Linux distro, BellSoft created Alpaquita Linux — a 100% Alpine-compatible distribution with the base image size of 3.32 Mb (musl) and 8.67 Mb (glibc). Apart from the small size, Alpaquita boasts several distinguishing features: Two libc implementations, optimized musl and glibc; Security hardening; LTS releases; Tools facilitating Java development and four mallocs for various Java workloads. You can use containers with Alpaquita Linux and Liberica JDK Lite for free. The migration is extremely easy and requires changing only one line in your Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-17-stream-musl as builder Alternatively, browse the Liberica Runtime Container repository for the most suitable image. In any case, you will get a lightweight container with the latest security patches both for JDK and Linux from one vendor as a result. For Spring Boot developers, there’s more — they will be able to save up to 30% RAM immediately after switching to Alpaquita containers! Conclusion To sum up, successful containerization rests upon the following principles: Determine how much memory your application requires; Set the -Xmx parameter and RAM limits for the container accordingly; Use the latest OpenJDK builds for your Java version; Tune the system, for example, by disabling swap. This article explains the fundamentals of the processes mentioned above. If you want to make a deeper dive into Java containerization practices, check out these materials: Dockerizing Spring boot apps with the smallest base image; How to use Linux containers; Java microcontainers with jlink; Are distroless containers really that small and secure? - [Garbage Collection in Java](https://bell-sw.com/announcements/2022/09/07/garbage-collection-in-java/): Our applications produce a lot of waste while in operation. Diligent Java garbage collectors remove the waste automatically, so we don’t have to bother with manual memory cleaning. But when a collector cannot handle their job, we immediately see that in the form of performance degradation. For instance, improper garbage collection settings may lead to noticeable pauses in your application’s work or increased memory usage. The first step in making garbage collection more efficient is understanding how the process is organized in Java, which GC implementations are there, and how to choose the right one for your app. Table of Contents How automatic Garbage Collection works What is Garbage Collection? How does Garbage Collection work? Advantages of automatic Garbage Collection Disadvantages of automatic Garbage Collection Types of Garbage Collectors in Java Serial Garbage Collector Parallel Garbage Collector Concurrent Mark Sweep Garbage Collector G1 Garbage Collector Z Garbage Collector Shenandoah Garbage Collector Epsilon GC A comparative table of Java garbage collectors Garbage Collection in GraalVM Native Image Garbage Collection and Java versions Garbage Collection best practices Conclusion How automatic Garbage Collection works What is Garbage Collection? Garbage collection is a process of freeing up memory by deleting unused objects from the heap. An object is considered eligible for GC when it becomes unreachable, meaning there are no references to it. Unlike C/C++, where the developer is in charge of destroying objects, GC in Java is automatic. There are ways of invoking the GC manually by calling System.gc(), but it is not the best practice. System.gc() may induce Full GC when the whole heap gets cleaned, and the method will wait until it is possible, which will have disastrous impact on performance. Before delving deep into GC implementations in Java, we should understand its mechanisms first. One of the functions of Java GC is managing objects in generations. Newly created objects belong to the young generation. As a rule, most objects “die young,” but some can move to the old or tenured generation. These generations occupy different spaces in the JVM heap: Metaspace replaces PermGen utilized in versions before Java 8. It stores metadata Eden space and two Survivor spaces (0 and 1) are where the young generation resides Tenured space preserves the old generation Java memory model When any of these spaces fill up, garbage collection happens. A minor collection occurs when the young space tops up. Most young objects don’t live long and are swept away, but if they are still used at the moment of garbage collection, they are moved to a Survivor space 0 or 1, and then to the tenured space. At some point, tenured space gets filled up, which causes a major collection. Major collections usually take more time because more objects are involved. How does Garbage Collection work? Now that we’ve discussed some essential concepts, how does GC work? When the application starts, the Eden space is empty. While the threads are working, the heap is filling up, and then an event triggers garbage collection. In the case of non-concurrent GCs, threads have to be stopped for a garbage collector to do its job. The GC marks unused objects and removes them. Other events that trigger the GC activation are: Insufficient memory for creating new objects; Explicit invocation of GC. After the memory gets cleaned, the threads are restarted. The pauses happening during the garbage collection are called Stop-the-World pauses. The key to optimal app performance is to keep the pauses minimal. Advantages of automatic Garbage Collection No need for manual memory allocation/deallocation, which saves the developer’s time and minimizes bugs related to human error; Efficient memory usage as the heap gets cleaned as soon as it fills up; Reduced risk of memory leaks — most of these cases are successfully handled by the collector. Disadvantages of automatic Garbage Collection Lack of control over memory management leaves little space for improving the resource usage; While some applications may benefit from automatic GC, larger enterprise apps may experience unpredictable performance deterioration; A memory leak that escaped the attention of a garbage collector is difficult to debug. Types of Garbage Collectors in Java Java provides numerous opportunities for GC tuning with various GC implementations with their own strengths. Primarily, the company establishes the most important performance indicators: throughput, latency, or footprint. The choice of a garbage collector depends on the defined goals. For instance, Parallel GC is the best option when throughput matters. On the other hand, G1 GC is aimed at low latency. After selecting the collector, you can start the tuning process based on GC capabilities. Garbage collection adjustment is a balancing act: large heap means longer pauses, short pauses lead to a more frequent GC activation and so on. Below you will find the summary of key garbage collectors available in JVM with distinguishing features. Serial Garbage Collector Serial GC is the oldest and simplest GC implementation in Java. It is utilized in single-threaded environments as it freezes all threads while it performs the collection and works in one thread itself. Serial GC is suitable for client-side applications without low pause requirements. Note that this GC will be used automatically if the RAM limit is set to less than 1792 MB or there are less than 2 CPUs. Otherwise, to enable Serial GC, use java -XX:+UseSerialGC -jar yourApp.java Parallel Garbage Collector Parallel GC or throughput collector is the default GC implementation suitable for multiprocessor environments. It also freezes all threads for garbage collection. But unlike Serial GC, it uses multiple threads to speed up the process. The developer can set the maximum thread number. For example, the following command java -XX:+UseParallelGC -XX:ParallelGCThreads=10 -jar yourApp.java will enable the Parallel GC with ten threads. But in a real-world scenario, the number of threads is calculated based on the processor number. Parallel GC uses multiple threads to sweep the young generation, but does with one thread when removing objects from the old generation. Starting with Parallel GC, it is possible to target the pause time (-XX:MaxGCPauseMillis) to maintain optimal throughput. Be careful not to set the value too small, though, or else the JVM will use a smaller heap to perform rapid garbage collection leading to the increased number of pauses. Another flag, -XX:GCTimePercentage, enables the developers to set the time the application spends on garbage collection. Concurrent Mark Sweep Garbage Collector CMS GC has two distinguishing features: It uses multiple threads to perform the collection; It shares processor resources with the application, i.e., it doesn’t freeze all the threads but utilizes some of them to do its job. This GC implementation is suitable for applications that benefit from short pauses and can afford to share the resources with the GC. CMS GC performs slower than Parallel GC or Serial GC, but it doesn’t stop the application. Since it performs garbage collection in concurrent mode, calling System.gc() will lead to concurrent mode failure. To enable CMS GC, run java -XX:+UseConcMarkSweepGC -jar yourApp.java Note that CMS GC has been deprecated since Java 9 to “accelerate the development of other garbage collectors in HotSpot” according to JEP 291, so you will get the following warning when trying to run it with Java 11: java -XX:+UseConcMarkSweepGC --version OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release. It has already been removed from the code base in newer versions, and the following message with Java 17 will pop up: Unrecognized VM option 'UseConcMarkSweepGC' G1 Garbage Collector G1 GC was designed to replace CMS GC with low latency in mind. It is fit for any application, but shows all its glory with server-style applications running a multiprocessor environment with a large heap (6+ GB). It splits the heap into multiple regions and performs the global marking phase to determine the liveliness of objects. After learning which heap regions are mostly filled with garbage, it first collects garbage in those regions to free up a lot of memory. This approach is called Garbage-First. G1 GC copies objects from one or several memory regions into a single region, which enables it to compact memory. The compaction is performed in parallel on multiprocessor machines, thus reducing pause times and increasing throughput. In addition, the developers can adjust the maximum pause time and pause time intervals. To enable G1 GC, run java -XX:+UseG1GC -jar yourApp.java Z Garbage Collector Z GC was introduced in Java 11 as an experimental feature and obtained production status starting with Java 15. It is a scalable low-latency collector that performs the expensive work concurrently and doesn’t stop the application threads for more than 10 ms. The most important parameter is the max. heap size (-Xmx): it should be able to accommodate the live-set of the app and provide enough room for allocations. To use Z GC, run java -XX:+UseZGC -jar yourApp.java For versions up to Java 15, the command will be slightly different java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar yourApp.java Shenandoah Garbage Collector Another low pause garbage collector is Shenandoah GC. It performs garbage collection concurrently with the running Java application thus reducing pause times which are not directly proportional to the heap size. Oracle Java doesn’t ship Shenandoah with any of its releases, but this GC is part of major OpenJDK distributions including Liberica JDK. Shenandoah works with all LTS releases and a current Java release and supports a wide range of platforms. The OpenJDK community continuously backports improvements and bug fixes to previous supported JDK versions. To enable Shenandoah GC, run java -XX:+UseShenandoahGC -jar yourApp.java Epsilon GC Epsilon GC is the most peculiar of all Java garbage collectors because it doesn’t collect any garbage. Its primary purpose is to allocate memory. Once the available heap is exhausted and the application tries to allocate more memory than allowed (i.e., set by -Xmx), the JVM shuts down with an OutOfMemoryError. This no-ops GC was introduced in Java 11 as an experimental feature, but it was decided to keep it experimental to avoid accidental Epsilon GC enabling in production. So, to use Epsilon GC, you need to explicitly enable experimental features first: java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -jar yourApp.java Despite the fact that Epsilon GC doesn’t collect any garbage, there are several use cases where it can be useful: Performance testing with Epsilon GC may reveal how fast the application runs without garbage collection and whether there are performance bottlenecks, which can be clearly seen without GC-induced performance artifacts. Applications that create all necessary objects at start and don’t produce any garbage as well as short-lived applications that don’t have time to spend all available resources may run faster without garbage collection. Otherwise, it is not recommended to use Epsilon GC to avoid unexpected application behavior and crashes. A comparative table of Java garbage collectors Characteristic Serial GC Parallel GC CMS GC G1 GC Z GC Shenandoah GC Epsilon GC Heap size Small Medium to Large Medium to Large Medium to Large Very large Very large N/A Latency High Moderate Moderate Moderate Low Low N/A Throughput Low High Moderate High High High N/A Use cases Client-sized application in the single processor environment Server-side application in the multiprocessor environment with focus on throughput Server-side application in the multiprocessor environment that can afford sharing the resources with GC Server-side applications in the multiprocessor environment with a large heap (~6 GB) Latency-sensitive applications, Applications with a very large heap (terabytes) Latency-sensitive applications, Applications with a very large heap (terabytes) Performance testing, Short-lived applications, Apps not producing garbage Garbage Collection in GraalVM Native Image What about the Native Image technology? How is memory managed there? Native images do not run on HotSpot but rather on GraalVM. Although the mechanism of garbage collection is the same, the range of GC implementations offered by the Native Image is slightly different: Serial GC is a default collector both in GraalVM Community and Enterprise Edition. It is aimed at delivering low footprint and minimal overhead at the expense of increased latency. The maximum heap size with Serial GC will be automatically set to 80% of available physical memory if the heap size is not explicitly specified with -Xmx or -XX:MaximumHeapSizePercent flags G1 GC is available in GraalVM EE only. It is optimized for optimal correlation between latency and throughput. By default, the maximum heap size is set to 25% of physical memory, but you can adjust this parameter. Other options of G1 GC tuning such as pause time, thread number, etc., are also available. Epsilon GC is available with GraalVM version 21.2 and higher. It doesn’t collect any garbage and is intended to be used with short running apps that allocate a small amount of memory. Find more information on GC tuning in native images in the official GraalVM documentation. Liberica Native Image Kit (NIK), a GraalVM-based utility for turning JVM-based applications into native executables, provides all these GC implementations. In addition, Liberica NIK is always based on the latest versions of GraalVM and Liberica JDK 11 or 17 with fixed bugs and improvements. Garbage Collection and Java versions The choice of a garbage collector relies, among other things, on the Java version you use. First of all, a certain collector may not be available with your version: CMS GC was removed from Java 14, so it is absent from newer JDK releases; Z GC appeared in Java 11 as an experimental feature and became ready for production-use in Java 15. However, it is absent in Java 8; Shenandoah GC was introduced in Java 12 as an experimental feature and became ready for production-use in Java 15, so it is absent from older Java versions; Epsilon GC was introduced in Java 11 and is not available in previous versions. Secondly, the performance of a chosen GC implementation also depends on the JDK version because there have been many enhancements introduced to Java garbage collection up until now, and the improvement process is on-going. Here are only a few examples of numerous GC changes in the latest Java versions: JEP 307: Parallel Full GC for G1 improves the latency of G1 GC during the full collection. JEP 439: Generational ZGC reduces the GC CPU overhead by making Z GC maintain young and old objects separately, thus collecting young objects more frequently. JEP 423: Region Pinning for G1 reduces latency by not disabling garbage collection in the presence of JNI critical regions. Just to get a taste how small this drop in the ocean is: as of August 22, 2024, Java Bug System contained 2,061 resolved and integrated fixes and enhancements to G1 GC alone! Therefore, upgrading the JDK version is key to using the full potential of Java garbage collectors. But if your enterprise workloads are based on JDK 8 or 11 and migration is off the table for now, you can use Liberica JDK Performance Edition that couples JDK 8 or 11 and JVM 17. So technically, you stay on JDK 8 or 11, but enjoy the performance of version 17, including new and improved GC implementation. The graph below shows the results of latency study with improved G1 GC and new Z GC for Spring Petclinic application based on Liberica JDK 8 Standard and Liberica JDK 8 Performance Edition: GC latency study with a standard JDK and Liberica JDK 8 Performance Edition More performance studies of Liberica JDK Performance Edition with improved garbage collectors can be found here. Garbage Collection best practices Garbage collection tuning is arduous and doesn’t have a “one-size-fits-all” solution, as everything depends on your application, the environment, and end goals. You can read more about JVM memory tuning options here. Here, we would like to give general recommendations on taking care of garbage collection: Don’t start tuning the GC when it is unnecessary. Small applications that don’t handle large amounts of data do well with automatic garbage collection. But even in the case of large enterprise apps, make sure that default GC settings are to blame before making changes. But even in this case, try switching to another GC, whose default setting may be more optimal for your application. There are three performance goals of GC tuning: throughput, latency, and footprint. Pick two and work towards them, as these goals usually compete. For instance, the more memory you allocate to your app, the better the throughput, but the longer the pause times. On the other hand, the smaller the heap, the lower the latency, but frequent pauses lead to deteriorated throughput. To adjust the GC parameters, you must understand how your JVM behaves and what is happening in the heap. Use Java monitoring tools such as JFR, Mission Control, jstats, or other profilers to gather the metrics on footprint, garbage collection, and performance in general. Learn to understand the GC logs that provide exhaustive information on garbage collector behavior, memory reclamation and allocation, pause length, and so on. To enable GC logs, run -Xlog:gc*::time It is recommended to store the logs in a dedicated file so that they don’t mix up with application logs. Calling System.gc() should be avoided, but you can help your GC differently by making the objects unreachable and thus eligible for GC. For instance, you can nullify or reassign the reference variable. Sometimes setting the min. (-Xms) and max. (-Xmx) heap size is enough to solve the issue. In some cases, more fine-grained tuning with extensive monitoring and benchmarking is required. But the deeper you dig into GC settings, the more fragile the solution will be. If the environment changes due to hardware upgrade, application scaling, etc., you may have to repeat the whole GC tuning process. GC tuning won’t magically enhance your app’s performance. If, after all the adjustments, you haven’t reached the goals, consider alternatives such as upgrading your hardware, OS, or refactoring the code. Conclusion This article described how automatic garbage collection is set up in Java. We summarized different GC implementations and their capabilities, so now you can choose an appropriate collector and start adjusting its settings. - [JDK 8 Maintenance Release 4: Important changes](https://bell-sw.com/announcements/2022/09/09/jdk-8-maintenance-release-4-important-changes/): Java SE 8 is the most reliable and stable JDK version still used by most developers. Regular CPU updates keep it safe and free of bugs, but this Maintenance Release 4 (MR4) due in October 2022 will bring some critical changes both to OpenJDK code and Java SE specification. JDK-8202260 — issue behind the changes JDK-8202260: Reference objects should not support cloning describes a critical issue identified in the Java SE 8 Platform. The semantics of cloning a reference object is not defined clearly in the Java SE Specification. Cloning is closely related to garbage collection, so if the reachability state of a reference object changes during GC activities, the collector may enqueue the object before the code calls the clone() method on it. As a result, the clone won’t be enqueued and referenced. This leads to highly unpredictable reference processing. The Reference.clone() was specified in Java SE 11 to always throw a CloneNotSupportedException. There are two related problems: There was no explicit prohibition to retrieve a referent of a phantom reference between the times it was enqueued and cleared by the GC. To fix this behavior, the Java SE 9 specification was updated The application was allowed to add a Reference object to the registered queue before it was cleared by the GC. It was also also adjusted in the Java SE 9 specification Now the changes introduced to Java SE 11 & 9 are backported into Java SE 8. How the MR4 will affect the app’s code There are two major changes that need to be introduced into the code of applications running on JDK 8: Reference::clone() will always throw a CloneNotSupportedException. A workaround for the code cloning referenced objects is to create a new reference object with a constructor Runtime.runFinalizersOnExit() will throw an UnsupportedOperationException. This method was deprecated in Java 1.2 due ro reliability issues and removed in Java 9. If the code utilizes runFinalizersOnExit(), it has to be refactored so as to remove the invocation Conclusion The new OpenJDK builds will be released in October. Please update your JDK version to keep your runtime safe from vulnerabilities and introduce aforementioned changes into your Java 8 program if necessary. - [How to create a single-node Kubernetes cluster](https://bell-sw.com/announcements/2022/09/14/how-to-create-a-single-node-kubernetes-cluster/): Great things have small beginnings — this couldn’t be more true when speaking of Kubernetes. Developers who have never worked with Kubernetes perceive it as a mighty beast doing wonders with container orchestration but too difficult to tame. Sooner or later, they will have to master this technology as it is becoming the standard of enterprise container management. Where to begin? The answer is — local clusters with one node. They imitate their big K8s brother perfectly and enable the developers to gain insights into the system without the risk of ruining anything. Besides, local clusters have another useful function — they can be used for JVM tuning in a sandbox mode. So read on to find out what local K8s clusters are and how to set them up! Table of Contents Kubernetes development with local clusters Overview of local Kubernetes solutions minikube K3s MicroK8s kind Comparative table Setting up minikube Install kubectl Install a hypervisor or Docker Install minikube Install NGINX Conclusion Kubernetes development with local clusters Kubernetes manages all workloads in clusters. The system doesn’t work with individual containers. Instead, it places them into pods to run on nodes, physical or virtual machines. Pods usually communicate with the outer world through an ingress controller. Nodes make up a cluster where they pool together their resources. The system redistributes the resources accordingly if any nodes are added or removed. A cluster may contain only one node for educational or experimental purposes. In addition, it can be placed locally on a developer’s machine or remotely in a cloud. The incredible Kubernetes journey starts locally, but even when the workloads shift to the cloud, local clusters can be of great help as they create a safe testing and development environment. But still, why do we need local clusters if we can start in a more realistic cloud environment right away? They provide a great learning tool for those who want to integrate K8s into their enterprise. K8s cluster architecture is anything but simple, and mastering the system's intricacies requires time and effort with quite a few knocks along the way. Local clusters imitate the work of a real Kubernetes perfectly, so the developers can get insights into the system and test its functionalities without the risk of ruining anything. Local clusters accelerate the feedback loop (when developers have to verify changes before pushing them into production) because they drastically reduce the time necessary for building images and pushing/pulling them to and from the cloud with all the security layers in place. They decrease the cost of development as application optimization can be performed locally without the need to control and monitor cloud resources usage. Local clusters provide a perfect sandbox environment great for testing, tuning, and proof of concept (PoC) experiments without the risk of blowing up the whole system. Overview of local Kubernetes solutions There are several open-source platforms for setting up a local Kubernetes environment. They are lightweight K8s distributions with a similar purpose of imitating a large-scale Kubernetes environment without using plenty of resources. The difference lies in the range of additional functions, so the choice depends on your business needs. minikube minikube is the most popular Kubernetes distribution developed by the Kubernetes project. It is effortless to install and use as a single-node K8s cluster with various operating systems, although it requires virtualization if run outside the Linux environment. In addition, minikube supports addons — the extensions for added Kubernetes functionality. You can add additional nodes to your cluster without any complications. minikube is perfect for testing purposes, but not fit for production deployments. K3s Created by Rancher, K3s is a sandbox K8s distribution tailored to low-resourced systems such as edge and IoT devices. It supports all OSs and is optimized for both ARM64 and ARMv7. K3s is easy to set up as a single-node cluster, but if you want to add nodes, you have to install K3s there and then connect them to the cluster. MicroK8s MicroK8s is developed by Canonical, the company behind Ubuntu. It is a modular, enterprise-grade Kubernetes distro that supports single- and multi-node clusters. MicroK8s can be used to run self-healing highly available clusters with multiple OSs. Another advantage is that it is compatible with edge and IoT devices and comes with optional commercial support. At the same time, installing and using the distribution is more complicated due to its modularity and multiple features to configure. kind kind (Kubernetes in Docker) was developed by the Kubernetes project primarily for testing K8s itself but can be used for CI purposes and local development. It builds Docker containers and uses them as nodes in a cluster. It enables the developers to create highly available multi-node clusters for Windows, macOS, and Linux. Comparative table minikube K3s MicroK8s kind Developer Kubernetes Rancher Canonical Kubernetes Operating systems Windows, Linux, macOS Windows, Linux, macOS Windows, Linux, macOS Windows, Linux, macOS Commercial support No No Yes No Installation Very easy Easy Easy with Linux snap support Easy Multi-node clusters Yes Yes, but requires effort Yes Yes Edge/IoT devices support No Yes Yes No Setting up minikube Our articles dedicated to cloud expenses reduction and Java Garbage Collection stressed the importance of JVM tuning for better app performance and efficient resources consumption. We can use local clusters to test various JVM configurations and pod limits. One node is often enough for that purpose. In our following experiments, we use a powerful x86_64 machine with 96 CPUs, so we need an easy-to-install local cluster, which has an Ingress and uses a local repository. We chose minikube based on these requirements. We will also need an Ingress to distribute the traffic through the replicas. From the outside it will look like a single entry point leading to multiple pods. We will use the NGINX Ingress controller as the Ingress and Docker as a driver to avoid overhead and virtualization consequences. Follow the steps below to install all necessary utilities for Linux. Windows and macOS users may refer to the official documentation. Install kubectl First of all, you need to install kubectl — a Kubernetes command-line tool. The major kubectl version should be within one minor version difference of a cluster: for instance, v1.25 is compatible with v1.24, v1.25, and v1.26. There are several methods of installing kubectl. The first one is to use curl. Run the following command to download the latest kubectl version: curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl Install kubectl with: sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl If you are using the Linux distribution with the snap package manager, you can use snap to install kubectl: snap install kubectl --classic As an alternative, you can use apt or yum package managers. kubectl is also available with Homebrew: brew install kubectl Regardless of the installation method, test the utility by checking the version: kubectl version --client Install a hypervisor or Docker minikube enables you to use a hypervisor to create and manage virtual machines on a local host. You can use KVM or VirtualBox for Linux. For our JVM tuning purposes, we will use Docker. Note that you can launch minikube directly on bare-metal without virtualization by stating --vm-driver=none when starting minikube. But running clusters without a VM layer is associated with decreased security, reliability, and data loss, so be cautious with the none parameter. Install minikube Now you are all set for installing minikube. You can perform the installation via a package manager, Homebrew, or by downloading the binary file. Let’s use curl to get the latest stable version for Linux x86_64: curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 sudo install minikube-linux-amd64 /usr/local/bin/minikube Use the following commands to be able to use minikube from any directory: sudo mkdir -p /usr/local/bin/ sudo install minikube /usr/local/bin/ Now you can start your cluster. Run minikube start --vm-driver=docker to use minikube with Docker. To make Docker the default driver, use minikube config set driver docker If you want to change the driver, you should stop minikube first (see below). Check that minikube is running correctly by requesting minikube status You should get a similar output: host: Running kubelet: Running apiserver: Running kubeconfig: Configured When you are done working with minikube, stop the cluster with minikube stop Install NGINX Ingress is an API object that manages access to Kubernetes services from outside the cluster. Ingress allows the developers to set up rules for routing traffic without creating numerous load balancers. Ingress Controller is the implementation of the Ingress API object. NGINX is a production-grade Ingress Controller with the following benefits: lightweight, easy-to-use, secure, and event-based. To enable the NGINX Ingress Controller (minikube must be started), run minikube addons enable ingress Check that NGINX is running: kubectl get pods -n ingress-nginx You should see something like this: NAME READY STATUS RESTARTS AGE ingress-nginx-admission-create-p8fsk 0/1 Completed 0 53s ingress-nginx-admission-patch-z6x9c 0/1 Completed 1 53s ingress-nginx-controller-755dfbfc65-229wz 1/1 Running 0 53s Conclusion Now when you have prepared your local Kubernetes cluster, you can start adjusting the JVM settings on a local machine without compromising the production environment. In the following article of the series, we learn how to deploy multiple application replicas to our cluster for the following load testing and JVM adjustments. - [Liberica JDK 19 is released](https://bell-sw.com/announcements/2022/09/21/liberica-jdk-19-is-released/): We are happy to announce the general availability of Liberica JDK 19. As it is the major Java version, it contains numerous fixes and enhancements: 2,422 fixes overall — 2,220 in JDK and 222 in FX. BellSoft engineers resolved 10 issues 7 JEPS with new or improved features You can download the builds now by heading straight to Liberica JDK Download Center. Or read on if you are wondering why you should bother with a non-LTS release! Non-LTS version — why should you care? With LTS versions coming out every two years and functional releases made available every six months, Java keeps pace with the latest IT trends. The emphasis in the development of the Java language is currently placed on the introduction of modern tools for efficient concurrent programming, so the highlights of this release are JEP 425: Virtual Threads (Preview) introduces lightweight threads not tied to an OS thread for the whole lifetime. Multiple threads can share one OS thread, so the application can use thousands of virtual threads to perform concurrent tasks. At the same time, they are easy to debug and profile and carry no risks associated with asynchronous programming JEP 428 Structured Concurrency (Incubator) is aimed at improving the reliability and performance of Java multithreading by coordinating the threads within a dedicated syntactic block The OpenJDK community doesn’t stop there, though. Apart from receiving APIs for performant and secure multithreading, Java enjoys better security (JEP 424: Foreign Function & Memory API), a more convenient coding process (JEP 405: Record Patterns), additional features for specific use cases (JEP 426: Vector API), and so on. A detailed analysis of all JDK 19 JEPs can be found in our article dedicated to the topic. The point is — the further away you are from the current LTS version, the fewer modern features you can utilize in your application. BellSoft supports all the previous Java versions, including the most popular Java 8 and even Java 6 & 7, so you are safe with us. But if high throughput is essential for your application, you will benefit most from new Java. Give JDK 19 a test drive and see what it is capable of — there is a high chance you will start planning your migration strategy! Download the latest version now! We understand that migration takes time and effort, but switching from Java 8 to 11 or even 17 is not impossible. BellSoft supports LTS versions longer than Oracle or any other OpenJDK vendor, so you can move at your own pace. Meanwhile, experiment with Java 19. A wide variety of new tools, enhanced security and concurrency will be a great incentive to embrace changes brought to you by the OpenJDK community! Download Liberica JDK - [JEP 429: Extent-Local Variables (Incubator)](https://bell-sw.com/blog/jep-429-extent-local-variables-incubator/): The nearest JDK 19 release is associated with a major event in the Java world — the introduction of virtual threads. It means that Java will get its own lightweight concurrency model, just like other modern programming languages. But there is still a lot of work to enhance and strengthen this feature. JEP 429: Extent-Local Variable targeted for Java 20 as an Incubator API is part of this effort. Motivation behind the JEP Thread-local variables have been a traditional way of sharing data between application components without using method arguments. They represent the ThreadLocal type and are usually declared as final static fields. They have multiple (one per thread) incarnations: code in each thread reads and writes its own distinct incarnation. These variables have inherent disadvantages: they are mutable and have an unbound lifetime and large overhead. The memory footprint increases with the number of threads used. Since newly introduced virtual threads can be quite plentiful (thousands or millions), memory consumption will be significant. Therefore, Java needs new per-thread variables to avoid costs of thread-local variables and increase the reliability of data flow in multithreaded applications. They must be immutable so that the child threads can efficiently share the data. They should also have a bounded lifetime to reduce the risk of memory leaks. Description Extent-local variables belong to the ExtentLocal type and have multiple incarnations, just like thread-local variables. But unlike their counterparts, extent-local variables are written once and then become immutable and available only during the thread’s execution. The name is derived from the “expect” concept used in the JVM specification. A method m1 in a given thread invokes a method m2, which invokes a method m3, and so on. While the methods haven’t been completed, their frames are stored in the JVM stack, where they are collectively called an extent. Extent-local variables written in m1 can be read in m2, m3, etc. The following code snippet demonstrates the usage of extent-local variables. final static ExtentLocal<...> V = new ExtentLocal<>(); // In some method ExtentLocal.where(V, ) .run(() -> { ... V.get() ... call methods ... }); // In a method called directly or indirectly from the lambda expression ... V.get() ... Note that the code structure delineates the period when a thread can read the incarnation of a variable. In addition, there is one-way data transmission from caller to callees. The absence of set() method means no remote code in the stack can change the variable, and the data can be reliably communicated to callees in the same thread. Immutability also increases the performance because an extent-local variable with get() is read as fast as a local variable. Extent-local variables will be helpful in many scenarios where thread-local variables are used now, especially when reliable one-way transmission of immutable data is essential. But there are several use cases where migration to extent-local variables is not preferable. For instance, when the data is transmitted in two ways (via set() method from one callee to another) or when code uses DateFormat objects, which are mutable per se. Conclusion The release of Java 20 is six months away, but the work on enhancing concurrency in Java is ongoing, so we are looking forward to news about other features targeted for the next release. Subscribe to our newsletter to stay tuned!Subscribe to newsletter - [BellSoft introduces Alpaquita Linux](https://bell-sw.com/blog/bellsoft-introduces-alpaquita-linux/): The best OS for containerized Java applications There are currently more than 300 actively maintained Linux distributions. “BellSoft created yet another Linux distro,” you might say, “so what?” Alpaquita Linux is not JUST another Linux distribution — it is the only Linux OS tailormade for Java! Based on Alpine Linux, Alpaquita: Enhances the advantages of this lightweight OS Solves the disadvantages related to security and support Contains new features for better performance Enables the effective containerization of Java applications developed with Liberica JDK Lets developers take advantage of native image technology through Liberica Native Image Kit support So, let’s get to know Alpaquita better! Contents Why we made Alpaquita What is the secret behind Alpaquita Linux? Alpaquita is performant Alpaquita is secure Alpaquita is reliable and enjoys long-term support Alpaquita lives in the smallest containers Conclusion Why we made Alpaquita The motivation behind creating Alpaquita Linux didn’t come out of nowhere. The existing distributions are either too heavy (therefore unsuitable for enterprise containers) or lack full security suite and LTS releases. In addition, there is no Linux optimized especially for Java applications. Our engineers have already developed the smallest container on the market with Alpine Linux and Liberica JDK, which is, for example, only 74 MB with Liberica JDK 17. We also provide a stable, TCK-verified runtime with the widest range of supported platforms and long-term support. But we decided to go even further. So we took Alpine Linux, enhanced its features, tailored it for Java applications, and thus created Alpaquita Linux — the best OS for containerized Java applications! The size of the base image of Alpaquita Linux is only 3.22 MB — almost the same size as Alpine’s base image. And it comes with support from the leading OpenJDK contributor, meaning you get the end-to-end solution for Java containers including the lightweight OS and the performant JDK with perfect platform compatibility and High-Powered Support for both! What is the secret behind Alpaquita Linux? Alpaquita Linux is a GNU/Linux distribution based on Alpine Linux. Despite being lightweight and optimally performant, Alpine has its shortcomings: Lack of modern security features Limited cloud and Java support No LTS releases musl library affects runtime performance Difficult migration process Our goal was to take the best features of Alpine, enhance them, and eliminate the disadvantages. As a result, we created a Linux distro with Optimal RAM consumption for Java deployment Java and GraalVM support to run in any infrastructure and on any cloud Top-notch security enhanced with modern features and CVE-tracking Support for Linux and Java Runtime with SLA Minimal static footprint for containerized applications Alpaquita is performant The OS’s performance is just as important as that of a runtime. Alpaquita’s performance is characterized by the following features: Kernel module compression support: compressed kernel modules reduce the size of kernel packages with a lot of modules, enabling faster installation and lesser memory consumption. Musl perf — Improved musl with support for indirect functions and changes integrated into the most loaded functions with assembler optimizations for CPU-specific commands (AVX512, EVEX, AVX2, SSE4, etc.) Developers can also use the standard musl version if necessary. Support for both musl and glibc libraries, solving problems with the migration. glibc support was added to satisfy possible demands from customers for glibc environment and to provide another option to leverage glibc performance vs musl. Note that glibc is not a lightweight solution, so the Docker images for the base Linux are different in this case, glibc-based version being 7.6MB in size. Optimized mallocs implementation with mimalloc, jemalloc, and rpmalloc included into Alpaquita Linux distribution. They can be used with different loads. Alpaquita is secure Security is a top priority for BellSoft, and Alpaquita Linux is no exception. This is how we overcame security gaps of Linux distributions: We work with the community and provide timely security patches to avoid zero-day vulnerabilities. Our proactive actions are aimed at preventing the exploitation of possible flaws. Kernel lockdown prevents both direct and indirect access to a running kernel image. Kernel module signing is based on SHA-512, disallowing the loading of unsigned modules or modules signed with an invalid key. Lack of extra components makes the distribution harder to attack. Userspace compilation options (-Wformat-security, -Wtrampolines, etc.) are aimed at additional security hardening. We provide fixes in alignment with the upstream Linux distribution and build the BellSoft Security Advisory, which includes listings of addressed CVEs, OpenJDK/Liberica JDK security advisory, tooling, and scanning. Alpaquita is reliable and enjoys long-term support LTS releases are recommended to enterprises, as they mean many years of support without the need for frequent updates, which is complicated in the case of large applications. Alpaquita Linux is currently aligned with Linux Kernel LTS — 5.10 Kernel LTS for Alpaquita Stream 22. BellSoft provides support for Alpaquita Linux for four years, which is two years longer than the maximum support period for Alpine Linux. Thanks to two years of overlap with the next LTS-release, you don’t have to hurry with the update and can continue using improvements, security patches, and bug fixes reported or demanded by customers. Release Release year End of commercial support Alpaquita LTS 22 2022 2026 Next Alpaquita LTS release 2024 TBD Support roadmap for Alpaquita Linux As far as support plans are concerned, three available options fit the demands of any enterprise or individual developer. You can use the Stream version of Alpaquita OS for free and receive security updates. But if you want to Work with a partner who is there for you 24/7 with response times as fast as 24 hours based on SLA Receive emergency patches Utilize the full power of Alpaquita for Java deployment in conjunction with other BellSoft solutions We offer you two commercial support packages, which we will cover in more detail in one of the next articles. Alpaquita lives in the smallest containers You will make the best out of containerization, as Alpaquita Linux is part of an end-to-end solution for containerized applications. It is packed in tiny containers with Liberica JDK, Liberica NIK, and your application. Find out more in the article on Alpaquita Cloud Native Platform! Conclusion In this article, we covered the highlights of Alpaquita Linux distribution. We encourage you to join in and discover the advantages of a fast, secure, and efficient distro fine-tuned for Java applications in containers. So download Alpaquita Linux, try out tiny containers with this new OS and Liberica JDK, or request a personal demonstration by clicking the button below. - [Docker Hub OpenJDK images are officially deprecated](https://bell-sw.com/blog/docker-hub-openjdk-images-are-officially-deprecated/): If you are using the official Docker Hub images of OpenJDK (open source implementation of the Java SE platform), your containers won’t receive any updates for the runtime environment anymore. Problem: Docker Hub deprecated OpenJDK images Since Java was released into the open source, it has been evolving within the OpenJDK project. Quarterly security updates help to keep the runtime free from security issues and are available to everyone for free. Some companies prefer using the “‘official” Docker Hub OpenJDK image. These are the “vanilla” builds based on Eclipse Temurin binaries and maintained by the Docker Community. But Docker Hub decided to stop updating this image in July 2022 and asked the users to find a suitable replacement as soon as possible. If you continue using the OpenJDK image without updates, your applications are going to face a severe risk of attacks exploiting unpatched vulnerabilities. Solution: Move to Liberica JDK image The only solution is to migrate to another binary offered by an OpenJDK vendor. You can use the Docker Hub Liberica JDK image. Liberica JDK is TCK-verified Java runtime so you won’t have to change your code; Supported by a major OpenJDK contributor BellSoft; Recommended by Spring and chosen by VMware as the runtime for the VMware Cloud; Included into the smallest Java containers on the market of only 37.92MB in size (Alpaquita Linux based images with JRE). To move from the OpenJDK image to Liberica Runtime Container (images with Alpaquita Linux optimized for Java applications and Liberica JDK Lite), change the line in the Dockerfile FROM openjdk:17 to FROM bellsoft/liberica-runtime-container:jdk-17-musl Inspired by Alpine, Alpaquita Linux was created with top security and flexibility in mind. It boasts Additional security features, LTS versions and enterprise-grade support, Two libc implementations, optimized musl and glibc; Four mallocs in total for various Java workloads. If you would like to learn more about our Linux distribution, head to Alpaquita Documentation with guides on installation, features, and container image optimization. BellSoft also provides Liberica JDK/JRE images for other popular Linux distributions, including Alpine. Head over to the BellSoft Docker Hub page to choose your perfect image! - [Introducing Alpaquita Cloud Native Platform for Java Applications!](https://bell-sw.com/blog/bellsoft-makes-a-breakthrough-in-the-world-of-java-with-alpaquita-cloud-native-platform/): Do you want to minimize expenses and increase profit at the same time? We have great news: we introduced Alpaquita Linux just now, an OS fine-tuned for Java applications, and it is time to present the Alpaquita Cloud Native Plaform — a perfect end-to-end solution for container deployment and TCO reduction! What is Alpaquita Cloud Native Platform? Components of Alpaquita Cloud Native Platform Liberica JDK Lite Alpaquita Linux Liberica NIK How using Alpaquita Cloud Native Platform cuts your expenses Tuned for Java End-to-end support for both Linux and Java Unified environment Ultimate security suite Save money and nature by embracing green IT Alpaquita containers: test drive Conclusion What is Alpaquita Cloud Native Platform? So what is Alpaquita Cloud Native Platform? As the name suggests, it is not a single tool but a complex solution for efficient cloud workloads. Alpaquita Cloud Native Platform (ACNP) is a technology stack bundled in a small container optimized for writing and executing Java applications. Each component of a stack is built and fine-tuned for your programs to be amazingly small, fast, stable, and secure. To be precise, the Platform consists of these software components: Liberica JDK Lite, a progressive Java runtime. This lightweight and secure open source Java Development Kit allows creating and executing Java applications on any platform or in any cloud. Alpaquita Linux, a brand new and the only Linux distribution tailor-made for running Java applications. Liberica Native Image Kit, a tool for creating native images. Using it with an application allows creating native images for cloud deployment saving RAM and accelerating startup. Together, they provide a comprehensive environment for development and cloud deployment. To see how, let’s take a closer look at each component. Components of Alpaquita Cloud Native Platform Liberica JDK Lite A runtime with the widest range of platforms, supported by a leading OpenJDK contributor. The version in the stack is Liberica JDK Lite, which is optimized for performance and minimal memory footprint. You can choose either JRE for deployment only or a JDK for development. Alpaquita Linux This new Linux distro is based on the lightweight Alpine Linux, but with the focus on security, enhanced performance, small size, and flexibility. It was created with a single goal in mind – to be the best environment to run Java applications. Read more about Alpaquita in this article to find how it is fine-tuned for Java applications and supports both musl and glibc. Alpaquita base image is only 3.22MB, which is 9x smaller than Debian slim and similar to Alpine, which is 2.67MB. Liberica NIK Liberica NIK is a GraavVM-based utility that converts applications into native executables ahead of time. It helps to accelerate the startup time up to 1/10 s and minimize memory footprint. Liberica NIK supports multiple platforms and languages and is perfect for building microservices. How using Alpaquita Cloud Native Platform cuts your expenses There are two essential tasks in software development among others: cutting the costs of development, and providing a stable, performant, and convenient solution to end users. Alpaquita Cloud Native Platform helps you achieve both goals with its unique properties: ACNP is tuned for Java Alpaquita Linux is an OS tuned especially for Java and including features that Increase the performance Provide faster startup without compromising other indicators Reduce latency of applications and ensure stability and consistency of operation Tiny ACNP containers are perfect for cases with extensive Java workloads because the pull time is dramatically reduced. And imagine how much you can save if 100 ms of latency cost 1–6% in sales! End-to-end support for both Linux and Java We provide long-term support for Liberica JDK and Alpaquita Linux with SLA, which means that you will Work with one reliable vendor Receive timely security patches and updates Stay on one version for as long as you need Reduce administration time and expenses tremendously and concentrate on other business tasks Unified environment Both the JDK and the OS are compatible with a wide range of platforms, infrastructures, and clouds. The solution provides you with a unified environment both for development and deployment. Spring developers will also appreciate the ACNP offering as Liberica JDK is a recommended runtime environment for Spring apps and Liberica NIK is the default native-image compiler used in Spring Native. Therefore, Spring apps will run in ACNP containers without any issues plus the developers will notice a boost in performance and reduced memory consumption. Consolidate your resources and leave compatibility issues in the past! Ultimate security suite Alpaquita Linux boasts hardened security as compared to Alpine Linux. BellSoft provides timely security fixes both for the OS and the JDK to avoid zero-day vulnerabilities, so your data will be protected at all times. Please remember that the consequences of a data breach due to security gaps can cost millions of U.S. dollars and destroy your business. Save money and nature by embracing green IT A special note should be made on how Alpaquita Cloud Native Platform helps you to both lower your costs and preserve the planet. With energy prices going up every day, green IT becomes a necessity more than a trend. ACNP was created with sustainable development in mind: Enhanced performance of ACNP components helps to maximize energy efficiency. Power consumption gets reduced and so do the power bills. By virtualizing the servers you spend less on hardware and reduce the burden on the planet. In cases when you need to utilize physical devices, optimal compatibility of ACNP enables you to use modern environment-friendly equipment and take care of nature and your employees’ health. Alpaquita containers: test drive Now, let’s check how Alpaquita Cloud Native Platform containers actually perform! First thing to take note of is the size of such containers. Alpaquita Cloud Native Platform container size glibc musl base 7.8 MB 3.22 MB Containing Liberica JDK Lite 79.27 MB 74.72 MB Next, we present the test where we put the “petclinic” app into the containers known for their small footprint and high performance, and compare them to Alpaquita Cloud Native platform containers and native images with the same app inside. Petclinic RAM footprint and latency As you see, Alpaquita Cloud Native Platform demonstrates better performance (up to 7%) and with utilization of Liberica NIK provides: Faster startup (up to 48x) Less RSS consumption (up to 50%) Lower latency (up to 25x at 99 pct) We are going to provide even more numbers that show the performance of Alpaquita in one of our next articles. Conclusion Alpaquita Cloud Native Platform guarantees significant cost reduction on multiple levels by reducing latency, optimizing memory, and increasing energy efficiency. Use it as a foundation for creating state-of-the-art technologies based on Java and enjoy enhanced stability, security, and long-term support. To find out more about the new Alpaquita Cloud Native Platform contact us and receive a first-hand demonstration of how ACNP will aid you in your endeavors! - [Alpaquita Linux performance — the race is on!](https://bell-sw.com/blog/alpaquita-linux-performance-the-race-is-on/): BellSoft recently introduced a Linux distribution tailor-made for cloud-native Java applications, Alpaquita Linux. Coming from a family of musl-based distros, Alpaquita boasts an impressive and unmatched advantage of having two standard C library implementations: optimized musl (musl perf) and glibc. Libc is a core OS component that provides the most widely used functions and serves as a bridge between the kernel and user programs. musl has a cleaner codebase than glibc, but the latter is more efficient in specific cases. We improved musl to eliminate existing performance issues and made glibc-based distro available, especially for companies that want to take advantage of the offering but are unwilling to migrate. In addition, Alpaquita Linux is Performant with kernel optimizations, musl-perf, and additional mallocs Secure with signed modules, kernel hardening, timely security patches, and security advisory Supported by a team of engineers who made the product. LTS releases, 24/7 commercial support, and regular updates make it the ultimate solution for enterprise Java-friendly with tools for Java development and full compatibility with other BellSoft products. It is a part of the Alpaquita Cloud Native Platform — a small container with the OS, Liberica JDK, and Liberica NIK Now, it’s high time we give it a ride! Alpaquita’s base image is only 3.22MB, but we will prove that it is as fast as a thoroughbred horse! To make the race more compelling, we will make Alpaquita compete with other popular Linux distributions and utilize industry-standard benchmarks as race tracks. Curious to know which Linux distros are the fastest and which bring up the rear? Find the results below! Table of Contents Methodology Results Startup Memory bandwidth String operations: glibc vs musl vs musl perf Throughput Performance of malloc implementations Java coupling with DaCapo Conclusion Methodology We ran the tests on the following machine, which is similar to many instances used in the cloud (for instance, AWS and Azure) or servers like Hetzner: Intel(R) Core(TM) i5-6600 CPU @ 3.30GHz 4 cores Full virtualization Type 1 hypervisor: KVM Type 2 hypervisor: QEMU A single VM was running on the machine as a workload, so it was dedicated to performance measurement. The following command was used to start the QEMU: qemu-system-x86_64 -cpu host -enable-kvm -smp 4 -m 4096 -device e1000,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5515-:22 -display none -daemonize -hda We utilized both musl- and glibc-based distributions to compare the performance of two libc implementations. As Alpaquita Linux has two variations, with glibc and musl, we tested both of them plus the stock musl-based variant to see whether there is a difference in results. Another musl-based distro used in experiments was Alpine Linux, which is the foundation for Alpaquita, but with stock musl and without performance optimizations. Regarding glibc-based systems, we selected CentOS, RHEL, and Debian as the most popular Linux distributions for servers and the cloud. A more detailed Linux Server/Cloud comparison can be found in our overview. Tested Linux distributions: Alpaquita Linux Stream v22 with glibc Alpaquita Linux Stream v22 with musl def (stock musl library) Alpaquita Linux Stream v22 with musl perf (optimized BellSoft musl) Alpine Linux v3.16 CentOS v9 RHEL v8 Debian v11 Rationale behind the chosen versions: Alpaquita Stream releases can be viewed as CentOS Stream type of releases. Therefore, Alpaquita Linux Stream 22 is not an LTS release, which will be out later this year as Alpaquita LTS v22 with subsequent updates to v22.1 and so on Alpine Linux v3.16 and CentOS Stream v9 are the latest releases Debian v11 is the latest stable version RHEL v9 was still in beta when the study was performed, so we utilized the latest stable version 8 at that time Results Startup The system startup time was measured at different stages: initramfs init: initialization of a root filesystem providing early userspace mounted root: the root filesystem is mounted login: the system allows to log in through console iface is up: the interface is up and running, which allows working with the console “network”: network services are connected We used Alpaquita with glibc and stock musl for this test. Note that the result deviation is about 4%. Startup time (s) OS load stage alpaquita-22-glibc alpaquita-22-musl alpine-3.16 centos-9 debian-11 redhat-8 initramfs init 0.14 0.14 0.20 0.75 0.64 0 mounted root 1.04 1.03 1.44 2.00 1.42 1.68 login 1.57 1.51 6.82 6.18 2.07 6.00 iface is up 1.95 1.86 3.41 3.94 1.96 3.77 "network" 2.98 2.89 4.47 5.35 2.98 5.14 Startup time results As you can see on the graph, both Alpaquita Linux configurations (glibc and musl) show the best results at all stages, followed closely by glibc-based Debian. At the login stage, which was a toughest stretch for other distros, musl-based Alpaquita loaded 77% faster than CentOS! Note that there is no data for Red Hat at the initramfs init stage. This is because Red Hat uses an older kernel version and doesn’t print out the timestamp for the first stage in dmesg. On the whole, Alpaquita Linux starts up the fastest, while CentOS and Red Hat demonstrate the worst results. Memory bandwidth We used the Stream benchmark to measure memory bandwidth, i.e., the memory volume we can use at a given time. Stream is a popular RAM benchmark measuring the sustainable main memory bandwidth in MB/s and the computation rate for simple vector kernels. It uses four kernels for different memory operations: Copy: transfer rate measurement without arithmetic operations Scale: adding a simple arithmetic operation Triad: chained/overlapped/fused multiply/add operations Add or Sum: adding a third operand We used the Stream benchmark implementation provided by Phoronix via the Phoronix Test Suite. Stream (MB/s) name alpaquita-22-glibc alpaquita-22-musl-def alpaquita-22-musl-perf alpine-3.16 centos-9 debian-11 redhat-8 Copy 21618.3 19071.5 21649.8 19198.8 21628.4 20522.1 20468.5 Scale 14745.8 14707.5 14744.8 14745.8 14748.7 14787.4 14752.6 Triad 16437 16441.1 16442.5 16474.9 16446.3 16482 16445.6 Add 16432.3 16428.2 16434.6 16466.6 16439.3 16473.8 16440.2 Stream results All distributions showed similar performance with Scale, Triad, and Add operations, but the most remarkable results were obtained with Copy operations. We can see that Alpaquita with optimized musl (musl-perf) shows the best performance (21618.3 MB/s), followed closely by CentOS (21628.4 MB/s) and Alpaquita with glibc (21618.3 MB/s). It is indicative of superior performance of BellSoft musl to the standard library implementation (musl-def). Note that musl perf is 100% compatible with the stock musl, so if you use a musl-based distro, the migration won’t cause any issues. String operations: glibc vs musl vs musl perf Many companies are hesitant about using a musl-based Linux in production due to inferior musl performance compared to glibc. To overcome the boundaries of a stock musl implementation, we developed musl perf with enhanced performance. The following tests with String operations evaluate the efficiency of three libc variants. We ran three series of hand-crafted tests with various String lengths: 34 chars, 132 chars, and 4100 chars because the performance may vary significantly depending on the String size. Why does it matter? Firstly, ISA-specific optimizations differ for various String lengths. Secondly, Strings of about 30 characters are more common, but the bigger the String, the longer the processing, so we can see the performance divergence more clearly. String operations 36 chars (ns/op) name alpaquita-22-glibc alpaquita-22-musl-def alpaquita-22-musl-perf alpine-3.16 centos-9 debian-11 redhat-8 strlen() 20.98 23.13 21.09 24.34 21.01 21.53 21.89 strnlen() 21.19 26.09 21.10 26.94 21.97 22.52 22.17 wcslen() 20.23 21.67 20.76 22.98 21.50 21.23 21.91 wcsnlen() 21.47 23.68 24.17 24.26 20.44 21.85 22.16 memcmp() 21.46 33.43 21.52 30.52 21.78 20.90 21.93 memset() 20.50 20.52 21.02 21.85 20.89 20.69 21.65 memmove() 20.59 27.69 21.21 28.08 21.10 21.69 21.68 memmove_fw() 20.88 64.47 21.55 65.46 20.64 21.02 21.70 memcpy() 20.84 31.80 21.74 30.89 19.94 20.90 21.80 strcmp() 21.15 29.47 21.70 35.43 22.25 21.47 22.23 strncmp() 21.47 45.18 21.57 49.76 22.82 21.21 22.46 strcpy() 21.60 24.86 22.09 26.07 21.37 21.46 24.39 strncpy() 22.21 28.91 22.91 28.64 23.44 22.06 24.97 strchr() 21.65 26.25 21.90 27.26 21.60 22.02 22.42 strrchr() 21.42 43.01 23.33 41.14 21.07 23.07 22.69 memmove_nop() 18.74 67.05 20.79 20.42 19.26 19.17 20.45 memcpy_r() 20.51 30.38 21.33 31.71 20.85 20.41 21.71 Results of String operations, 36 chars String operations 132 chars (ns/op) name alpaquita-22-glibc alpaquita-22-musl-def alpaquita-22-musl-perf alpine-3.15 alpine-3.16 centos-9 debian-11 redhat-8 strlen() 21.97 29.58 22.97 30.62 29.84 22.33 22.05 23.06 strnlen() 22.89 39.96 22.73 37.10 35.38 23.62 22.30 23.33 wcslen() 22.24 36.09 22.34 29.16 29.64 23.00 22.21 23.05 wcsnlen() 23.25 34.88 30.66 35.06 36.88 24.52 22.25 23.55 memcmp() 22.70 89.81 22.49 85.20 68.50 23.81 22.35 23.60 memset() 21.01 22.25 21.29 22.20 22.68 22.21 20.74 21.96 memmove() 20.91 28.56 21.24 27.70 27.93 21.94 21.39 22.22 memmove_fw() 21.44 78.32 21.43 77.39 77.94 21.93 21.17 22.30 memcpy() 21.05 33.56 21.30 34.45 33.44 22.09 21.05 22.27 strcmp() 22.86 72.97 23.10 75.26 81.95 77.49 22.83 23.70 strncmp() 23.57 139.10 23.12 137.56 138.50 93.24 23.50 23.79 strcpy() 27.80 36.92 28.79 33.12 39.50 28.27 27.48 24.53 strncpy() 29.36 38.39 29.89 37.76 38.37 30.63 27.84 25.96 strchr() 23.12 44.19 23.33 36.67 37.81 23.46 23.40 23.91 strrchr() 23.51 107.08 24.39 110.72 103.69 24.99 23.95 24.37 memmove_nop() 19.16 19.73 19.57 92.80 20.41 19.89 19.94 20.42 memcpy_r() 21.50 33.68 21.73 33.66 34.44 23.46 21.23 22.28 Results of String operations, 132 chars String operations 4100 chars (ns/op) name alpaquita-22-glibc alpaquita-22-musl-def alpaquita-22-musl-perf alpine-3.16 centos-9 debian-11 redhat-8 strlen() 42.60 300.76 43.87 299.58 44.01 48.62 50.80 strnlen() 53.48 438.14 46.88 393.59 57.60 51.24 52.32 wcslen() 44.65 373.87 43.50 376.22 44.90 56.57 56.27 wcsnlen() 56.67 476.98 297.37 476.68 56.65 48.36 55.69 memcmp() 71.57 1458.78 72.71 1455.40 71.07 72.89 74.41 memset() 86.89 90.98 86.45 92.00 62.55 87.03 88.14 memmove() 36.46 50.69 36.80 45.69 37.88 39.46 36.64 memmove_fw() 37.36 596.58 37.08 595.35 38.68 39.21 39.03 memcpy() 55.86 91.37 56.08 97.52 56.11 56.88 56.53 strcmp() 83.81 1468.49 83.01 1461.64 86.63 82.32 81.47 strncmp() 82.92 3480.92 90.03 3476.01 88.32 88.02 86.67 strcpy() 83.25 452.00 81.90 448.43 83.60 82.54 83.46 strncpy() 84.68 508.51 83.67 507.27 83.65 84.40 82.92 strchr() 65.75 526.52 63.13 504.89 66.62 77.08 77.93 strrchr() 90.48 2483.10 89.17 2445.37 90.62 90.31 90.83 memmove_nop() 18.62 1132.01 20.62 20.41 19.32 19.23 20.53 memcpy_r() 53.79 113.54 54.47 121.52 54.42 54.25 55.31 Results of String operations, 4100 chars All three graphs show a significant discrepancy between the performance of standard musl-based systems and glibc-based ones: stock musl demonstrated the worst results in all tests. We can also see that the results of improved musl (Alpaquita with musl-perf) are equal to those of glibc. It means that optimizations we introduced into musl will enable companies to use our compact distribution Alpaquita Linux without the detrimental impact on application performance. Throughput To test the throughput of Linux distributions, we used the Nginx benchmark provided by Phoronix. Nginx is a highly performant open-source web server that can also be used for reverse proxying, load balancing, caching, and other tasks. Its purpose is to control the workload and protect the backend. Nginx Ingress Controller is commonly used with Kubernetes clusters for routing traffic without creating numerous load balancers. The Nginx benchmark runs on a single host and measures the number of HTTP requests handled per second with a configurable number of concurrent clients. Nginx (reqs/s) name alpaquita-22-glibc alpaquita-22-musl-def alpaquita-22-musl-perf alpine-3.16 centos-9 debian-11 redhat-8 Concurrent Requests: 1 21905.96 21811.14 21866.25 21362.84 15717.46 20528.18 16800.98 Concurrent Requests: 20 77072.15 76141.41 76053.68 69856.36 55991.42 55019.88 61122.7 Concurrent Requests: 100 85526.76 85256.34 84218.03 77749.41 61052.06 55717.28 63137.18 Concurrent Requests: 200 85082.81 82633.25 83891.91 76352.81 59159.26 56608.38 62187.37 Concurrent Requests: 500 81313.28 78921.85 80128.55 75187.77 54158.91 58064.64 60493.26 Concurrent Requests: 1000 79049.85 78305.92 78541.19 72996.01 53429.85 57546.15 59431.69 Nginx results All three Alpaquita configurations demonstrated the best results across all tests, followed by Alpine. It is also remarkable that glibc-based Alpaquita outperformed other glibc-based distros significantly, which means that Alpaquita may be a good choice for companies that do not want to migrate from glibc but put a focus on good application throughput. Performance of malloc implementations Linux memory allocators (mallocs) can accelerate or slow down the applications. For instance, in the case of musl, switching to mimalloc or jemalloc may solve some performance issues. As far as JVM is concerned, it allocates user objects in the Java heap and collects garbage, but the JVM code uses the system allocator, so choosing a correct malloc is essential for Java apps. Alpaquita Linux comes with three additional malloc implementations for different environments: mimalloc is a small allocator used in large scalable services with low latency rpmalloc is the tiniest allocator with lock free thread caching function jemalloc enables the developers to solve fragmentation issues and supports scalable concurrency To test the work of memory allocators in various Linux distributions, we used a Mimalloc-bench originally developed for mimalloc, but later adopted for other implementations as well. The tests measure the number of operations performed in one second. malloc benchmarks utilized in the study espresso: a programmable logic array analyzer in the context of cache aware memory allocation barnes: a hierarchical n-body particle solver [4], simulating the gravitational forces between 163840 particles alloc-test: simulates intensive allocation workloads with a Pareto size distribution cache-thrash: part of Hoard benchmarking suite, designed to exercise heap cache locality cache-scratch: introduced with the Hoard allocator to test for passive-false sharing of cache lines mstress: simulates real-world server-like allocation patterns, using N threads with with allocations in powers of 2 where objects can migrate between threads and some have long life times Alpaquita configurations with mallocs: alpaquita-22-musl-perf-je — Alpaquita with musl-perf + jemalloc alpaquita-22-musl-perf-mi — Alpaquita with musl-perf + mimalloc alpaquita-22-musl-perf-rp — Alpaquita with musl-perf + rpmalloc malloc performance (operation, 1/s) name alpaquita-22-glibc alpaquita-22-musl-def alpaquita-22-musl-perf-je alpaquita-22-musl-perf-mi alpaquita-22-musl-perf alpaquita-22-musl-perf-rp alpine-3.16 centos-9 debian-11 redhat-8 espresso 0.19 0.13 0.21 0.22 0.13 0.22 0.12 0.20 0.20 0.20 barnes 0.36 0.37 0.37 0.38 0.37 0.37 0.37 0.38 0.36 0.36 alloc-test1 0.22 0.14 0.27 0.29 0.14 0.25 0.14 0.23 0.24 0.23 alloc-test-4 0.22 0.02 0.26 0.28 0.02 0.25 0.01 0.22 0.23 0.22 cache-thrash-1 0.71 0.71 0.71 0.71 0.71 0.71 0.72 0.72 0.64 0.64 cache-thrash-16 2.63 2.63 2.63 2.70 2.70 2.63 2.70 2.70 2.38 2.38 cache-thrash-4 2.70 2.70 2.70 2.70 2.70 2.70 2.70 2.70 2.38 2.38 cache-scratch-1 0.71 0.72 0.71 0.71 0.71 0.71 0.72 0.72 0.64 0.64 cache-scratch-16 2.70 2.63 2.63 2.63 2.70 2.63 2.70 2.70 0.86 1.20 cache-scratch-4 2.70 2.70 2.70 2.70 2.70 2.70 2.70 2.70 0.15 0.15 mstress-1 0.17 0.13 0.23 0.39 0.13 0.16 0.12 0.17 0.17 0.17 mstress-16 0.14 0.07 0.17 0.24 0.07 0.13 0.06 0.13 0.14 0.13 mstress-4 0.171 0.124 0.231 0.368 0.124 0.160 0.118 0.166 0.170 0.170 malloc benching results The above graph indicates that no memory allocator excels in all conditions. For instance, the glibc malloc and three optimized musl mallocs (mimalloc, rpmalloc, jemalloc) showed the best results in espresso and alloc-test Debian and Red Hat default mallocs demonstrated the worst results in all cache-scratch and cache-thrash tests, whereas other allocators demonstrated similar performance, with musl perf showing stable superior performance in all cases mimalloc was a clear leader in all mstress tests To sum up, a variety of malloc implementations and the possibility to choose between glibc and musl to use with Alpquita gives additional flexibility to system configuration. The choice of a malloc implementation should be driven by workloads it will be used with. Learn more about choosing a suitable malloc Java coupling with DaCapo DaCapo benchmark suite is a set of real-world Java applications with different memory loads used to evaluate system/CPU performance. The results are measured in ms required for the completion of a workload. We used Alpaquita with different malloc implementations in this benchmark to assess whether they improve the situation in cases when stock musl demonstrates inferior performance to glibc. DaCapo benchmarks utilized in the study h2: executes a JDBCbench-like in-memory benchmark, executing a number of transactions against a model of a banking application fop: takes an XSL-FO file, parses it and formats it, generating a PDF file pmd: analyzes a set of Java classes for a range of source code problems xalan: transforms XML documents into HTML avrora: simulates a number of programs run on a grid of AVR microcontrollers jython: interprets a the pybench Python benchmark luindex: uses Lucene to indexes a set of documents sunflow: renders a set of images using ray tracing lusearch: uses Lucene to do a text search of keywords over a corpus of data comprising the works of Shakespeare and the King James Bible tradebeans: runs the daytrader benchmark via a Java Beans to a GERONIMO backend with an in memory h2 as the underlying database DaCapo (ms) name alpaquita-22-glibc alpaquita-22-musl-def alpaquita-22-musl-perf-je alpaquita-22-musl-perf-mi alpaquita-22-musl-perf alpaquita-22-musl-perf-rp alpine-3.16 centos-9 redhat-8 h2 3119 3270 3098 3099 3264 3104 3217 3235 3725 fop 1115 1232 1107 1104 1249 1120 1230 1167 986 pmd 1577 1716 1587 1592 1746 1658 1757 1611 1571 xalan 2018 2133 2057 2003 2143 2061 2152 1977 1706 avrora 3675 3765 3589 3646 3738 3628 3844 3232 3327 jython 4454 4705 4342 4412 4711 4390 4609 4554 3904 luindex 876 977 862 860 959 868 953 895 826 sunflow 2349 2438 2216 2288 2421 2332 2468 2386 2162 lusearch 1328 1451 1331 1335 1423 1436 1559 1411 1203 tradebeans 3649 3886 3539 3539 3917 3613 4039 3735 3159 DaCapo results In all sets, — except for h2 where the leadership belongs to glibc in Red Hat, — musl perf (Alpaquita) and stock musl (both Alpaquita and Alpine) demonstrated the best results as compared to glibc. Remarkably, Java applications benefit from musl more than from glibc in these scenarios. Companies with a Java-based project may be recommended to test their applications in a musl environment and note the changes in performance. However, it is important to measure other metrics, such as latency, footprint, etc., because libc implementations may behave differently with specific workloads. Conclusion The benchmarking results show that Alpaquita Linux outperforms other Linux distributions for startup and throughput In cases where glibc demonstrated superior results to stock musl, Alpaquita Linux with optimized musl has similar or superior results compared to glibc-based distros glibc-based Alpaquita gives better results for startup and throughput as compared to other glibc-based distros and is equal to them in other cases There is no perfect malloc for all use cases, but the fact that Alpaquita Linux has three additional mallocs means greater variability in performance tuning musl-based distributions demonstrated better results in the DaCapo benchmark suite, which indicates better musl suitability for specific workloads. But further testing with additional metrics is required to choose an optimal solution It turns out, Alpaquita won the race hands down! It is fast, reliable, and incredibly flexible, providing the best options for enterprises: Optimized musl for those who want to save cloud resources but are unwilling to deal with performance issues of stock musl glibc package for cases when migration to another libc is undesirable or complicated. For cases when startup and throughput are crucial, it can even be more advantageous than traditional glibc-based systems Three additional memory allocators for various use case scenarios And, of course, Commercial support with LTS releases and strict update schedule Tools for Java development A unified solution for cloud-native Java deployment with Liberica Lite and Liberica NIK on board — Alpaquita Cloud Native Platform So let Alpaquita Linux loose with your application riding a perfect seat and see that it is a total game-changer! Download Alpaquita Linux for free - [Liberica 8u352, 11.0.17, 17.0.5, and 19.0.1 builds are generally available](https://bell-sw.com/blog/liberica-8u352-11-0-17-17-0-5-and-19-0-1-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 8u351, 11.0.16.1, and 17.0.4.1. CPU releases include patches for Common Vulnerabilities and Exposures (CVE). In addition, we release PSU versions 8u352, 11.0.17, 17.0.5, and 19.0.1 with non-critical fixes and general improvements. The release contains 601 fixes and backports overall. BellSoft participated in eliminating 16 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 6 security issues (CVEs) fixed 34 total security fixes in CPU release: in Liberica 8u351: 10 security fixes + 0 in FX in Liberica 11.0.16.1: 13 security fixes + 0 in FX in Liberica 17.0.4.1: 11 security fixes + 0 in FX In addition, PSU releases include a total of 567 bugs and backports fixed: in Liberica 8u352: 4 security fixes + 50 additional fixes (+ 9 in FX) in Liberica 11.0.17: 6 security fixes + 217 additional fixes (+11 in FX) in Liberica 17.0.5: 5 security fixes + 222 additional fixes (+12 in FX) in Liberica 19.0.1: 5 security fixes + 26 additional fixes Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2022-21618 5.3 security-libs org.ietf.jgss network low none none unchanged none low none CVE-2022-21619 3.7 security-libs java.security network high none none unchanged none low none CVE-2022-21624 3.7 core-libs javax.naming network high none none unchanged none low none CVE-2022-21626 5.3 security-libs java.security network low none none unchanged none none low CVE-2022-21628 5.3 core-libs java.net network low none none unchanged none none low CVE-2022-39399 3.7 core-libs java.net network high none none unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 19 CVE-2022-21626 • • - - CVE-2022-21618 - • • • CVE-2022-21628 • • • • CVE-2022-39399 - • • • CVE-2022-21619 • • • • CVE-2022-21624 • • • • Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Liberica Native Image Kit 22.3.0 and 21.3.4 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-22-3-0-and-21-3-4-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 22.3.0 and 21.3.4 as part of Critical Patch Update (CPU) release cycle. The builds contain several security fixes and enhancements. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable changes Improved module system support. When building JavaFX native images with the Full version, there is no need to list JavaFX modules on the command line with --add-modules javafx.controls,... because all modules that are part of JDK are visible by default Added support for JFR events: jdk.JavaMonitorEnter, jdk.JavaMonitorWait, jdk.ThreadSleep Added support for heap dumps Added new class initialization strategy that allows all classes to be used at image build time Added support for ThreadMXBean#getThreadCpuTime Introduced the --enable-monitoring option Summary of fixes and enhancements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2022-21618 5.3 security-libs org.ietf.jgss network low none none unchanged none low none CVE-2022-21619 3.7 security-libs java.security network high none none unchanged none low none CVE-2022-21624 3.7 core-libs javax.naming network high none none unchanged none low none CVE-2022-21626 5.3 security-libs java.security network low none none unchanged none none low CVE-2022-21628 5.3 core-libs java.net network low none none unchanged none none low CVE-2022-39399 3.7 core-libs java.net network high none none unchanged none low none Summary of fixes in Liberica NIK CVEs fixed in Liberica NIK per version: CVE ID 22.3.0 (JDK 11) 22.3.0 (JDK 11) 21.3.4 (JDK 17) 21.3.4 (JDK 17) CVE-2022-21626 • • - - CVE-2022-21618 • • • • CVE-2022-21628 • • • • CVE-2022-39399 • • • • CVE-2022-21619 • • • • CVE-2022-21624 • • • • Conclusion BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [Eight Java features you should start using now!](https://bell-sw.com/blog/eight-java-features-you-should-start-using-now/): The newest Java release includes a bunch of new and exciting features such as virtual threads or structured concurrency. But even if you are using an older JDK version, Java still has some aces up its sleeve! This article will guide you through useful Java features that make developer’s life easier and apps more performant. Most of these treasures are hidden in JDK 8+. Some features are new and thus will give you a great incentive to finally migrate to the latest LTS release! Methods and constructors in enum DelayQueue Shutdown Hooks Modules Double-brace initialization Instance initializers Phaser Pattern matching for switch Conclusion 1. Methods and constructors in enum enum types are Java classes defining collections of constants. In their simplest and most widely used form they look like this: public enum Vehicle { CAR, BUS, BICYCLE, SCOOTER } However, Java enums are much more powerful and allow the developers to add fields, methods, interfaces, etc. They are Comparable and Serializable and can implement all the Object methods. For instance, let’s create an enum Animal and assign a proverb to each value: public enum Animal { DOG("A barking dog doesn't bite"), CAT("Curiosity killed a cat"), RAT("Rats desert a sinking ship"); private final String proverb; Animal(String proverb) { this.proverb = proverb; } public String getProverb() { return proverb; } } We can then iterate through the values by means of a static valueOf() method: public class Main { public static void main(String[] args) { for (Animal a : Animal.values()) { System.out.println(a.name() + " - " + a.getProverb()); } } } The output will be as follows: DOG - A barking dog doesn't bite CAT - Curiosity killed a cat RAT - Rats desert a sinking ship Each constant can be assigned a different behavior for a specific method. For instance, by making a main method abstract and overriding it in each constant: public enum Operation { PLUS { double evaluate(double x, double y) { return x + y; } }, MINUS { double evaluate(double x, double y) { return x - y; } }; abstract double evaluate(double x, double y); } All these capabilities enable convenient work with any type of constants. 2. DelayQueue DelayQueue is part of java.util.concurrent package. It is a blocking queue that orders elements based on the delay time. Elements can be taken from the queue only when their delay has expired. The head element is the one whose delay has expired earlier. If there is no delay expiration, the poll will return null. All objects have to belong to the Delay class or extend the Delayed interface. import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class DelayObject implements Delayed { private String name; private long startTime; public DelayObject(String name, long delayInMilliseconds) { this.name = name; this.startTime = System.currentTimeMillis() + delayInMilliseconds; } @Override public long getDelay(TimeUnit unit) { long diff = startTime - System.currentTimeMillis(); return unit.convert(diff, TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { if (this.startTime < ((DelayObject) o).startTime) { return -1; } if (this.startTime > ((DelayObject) o).startTime) { return 1; } return 0; } } When the getDelay(TimeUnits.NANOSECONDS) method returns a value less than or equal to zero, the delay expires. In addition, this queue is unbounded meaning it can store any number of elements. DelayQueue is helpful for controlling the intervals of data processing. The application logic can be set in such a way so as to perform tasks at certain intervals regardless of the number of submitted tasks. This way, the application will not be overloaded and perform smoothly. 3. Shutdown Hooks JVM can shutdown in two ways: in a controlled manner and abruptly due to external forces. Although we can’t influence the JVM behavior in case of abrupt shutdown, we can structure our code to perform some house-keeping tasks (release resources, etc.) before a controlled shutdown. This is achieved with shutdown hooks. They belong to the Thread class and are initialized but unstarted threads. When the JVM starts the shutdown process, it will start all registered shutdown hooks in an unspecified order and run them concurrently. When all hooks are finished, the JVM will halt. Shutdown hooks are easy to create: Thread hook = new Thread(() -> System.out.println("Shutting down, bye!")); Runtime.getRuntime().addShutdownHook(hook); Note that the JVM will execute the hooks only when it terminates normally. If it aborts due to kill -9 signal (Unix) or TerminateProcess (Windows) Runtime.getRuntime().halt() Power failure or other situation killing the OS there is no guarantee that shutdown hooks will execute. 4. Modules Modules introduced in JDK 9 enable the developers to write Java applications as a set of modules rather than an indivisible piece of software. A module includes closely related packages, resources, and a module descriptor file. Modularity brings enormous benefits to Java development: it makes the code stable and reusable and speeds up the development process because the developers can write the code in parallel and test the modules on the fly. Starting with Java 9, JDK itself is organized in modules. If you run java --list-modules You will get the list of modules your JDK distribution uses, where java modules are implementation classes for the core Java SE specification, jdk modules are libraries used by the JDK, and javafx modules (if you have a Liberica Full version) contain FX UI libraries. It means that modularity can be used to create a custom JDK by using only the modules required by the application. The containers created this way consume several times less memory than standard Docker containers and thus minimize resource consumption and cloud expenses. You can create a modular JDK yourself or use a ready solution developed by BellSoft — microcontainers with Liberica Lite and Alpaquita Linux, our own lightweight Linux distribution optimized for cloud-native applications. 5. Double-brace initialization Starting with Java 9, double braces can be used to create and initialize classes in a single expression. This feature should be used to add elements to HashMap, which can only be initialized in a constructor. We do not recommend utilizing it for other purposes for the sake of code readability and easy debugging. Below is a traditional way of creating and populating a HashMap: Map data = new HashMap(); data.put(1,"value1"); data.put(2,"value2"); But we can shorten the code by using double braces: Map data = new HashMap(){{ put(1,"value1"); put(2,"value2"); }}; Here, we created an anonymous subclass of HashMap and provided instance initialization to add two elements. Double-brace initialization may be perceived as syntactic sugar, which doesn’t affect app’s performance but makes the coding process more convenient if you work with HashMaps. In this case, the feature reduces the notorious Java verbosity by minimizing boilerplate code. 6. Instance initializers Technically, you can use static initialization for everything, but you have to be extremely careful with this feature in Java because of the added complexity and unexpected behavior in some cases, for example, when using native image technology. It is recommended to share code between constructors. Instance initialization blocks are used to initialize instance data members. Instance blocks run every time an object of the class is created. Initializers execute in the order they are defined in the class. They are also invoked after the parent class constructor invocation. Consider the following code snippet: public class Dog { public Dog() { System.out.println("Dog constructor"); } { System.out.println("Dog instance initializer"); } } public class Puppy extends Dog { public Puppy() { System.out.println("Puppy constructor"); } { System.out.println("Puppy instance initializer #1"); } { System.out.println("Puppy instance initializer #2"); } } public class Main { public static void main(String[] args) { Puppy jack = new Puppy(); Puppy sam = new Puppy(); } } The output will be: Dog instance initializer Dog constructor Puppy instance initializer #1 Puppy instance initializer #2 Puppy constructor Dog instance initializer Dog constructor Puppy instance initializer #1 Puppy instance initializer #2 Puppy constructor Initializer blocks are copied into every constructor, so they come handy when you have multiple constructors and have to use common code for them. 7. Phaser Another valuable component of java.util.concurrent package is Phaser. It is similar to CountDownLatch and CyclicBarrier that allow thread coordination, but is more functional and flexible. Phaser enables the developers to synchronize the dynamic number of threads that need to wait on a barrier before proceeding the execution. Unlike other barriers, the Phaser barrier can be reused for all program phases, and the number of threads may vary in each phase. It can synchronize one or multiple phases whereas a CyclicBarrier supports only a single-phase synchronization. Let’s take a look at a simple class for coordinating multiple phases. import java.util.concurrent.Phaser; class PhaserThread implements Runnable { private String thread; private Phaser phaser; PhaserThread(String thread, Phaser phaser) { this.thread = thread; this.phaser = phaser; phaser.register(); } @Override public void run() { System.out.println(thread + " starting phase " + phaser.getPhase()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread + " finished work, waiting for others"); phaser.arriveAndAwaitAdvance(); phaser.arriveAndDeregister(); } } We are registering to the Phaser with a register() method. The arriveAndAwaitAdvance() method makes all current threads wait on the barrier until the last thread finishes its job. After that, threads deregister themselves with the arriveAndDeregister() method. You should pass 1 (the coordinator thread) as an argument when creating a Phaser instance in the main app which is the same as calling register() from the thread. public static void main(String[]args) { ExecutorService executorService=Executors.newCachedThreadPool(); Phaser phaser=new Phaser(1); executorService.submit(new PhaserThread("Thread A",phaser)); executorService.submit(new PhaserThread("Thread B",phaser)); executorService.submit(new PhaserThread("Thread C",phaser)); phaser.arriveAndAwaitAdvance(); System.out.println("Phase "+phaser.getPhase()+" is completed"); executorService.submit(new PhaserThread("Thread D",phaser)); executorService.submit(new PhaserThread("Thread E",phaser)); phaser.arriveAndAwaitAdvance(); System.out.println("Phase "+phaser.getPhase()+" is completed"); phaser.arriveAndDeregister(); } The output will be similar to Thread B starting phase 0 Thread A starting phase 0 Thread C starting phase 0 Thread C finished work, waiting for others Thread B finished work, waiting for others Thread A finished work, waiting for others Phase 1 is completed Thread D starting phase 1 Thread E starting phase 1 Thread D finished work, waiting for others Thread E finished work, waiting for others Phase 2 is completed Another benefit of Phaser is that it can be tiered, i.e., constructed in tree structures where a group of sub-phasers has a common parent. It helps to avoid heavy contention costs and increase throughput. 8. Pattern matching for switch Pattern matching for switch is a feature introduced in Java 17 and aimed at enhancing the experience of working with switch expressions and statements. Previously, switch statements could take only a few types of values — numeric types, enums, String — and test only the exact equality. We had to resort to numerous if… else blocks to use patterns with switch before Java 17, which would look like this: static String formatter(Object o) { String formatted = "unknown"; if (o instanceof Integer i) { formatted = String.format("int %d", i); } else if (o instanceof Long l) { formatted = String.format("long %d", l); } else if (o instanceof Double d) { formatted = String.format("double %f", d); } else if (o instanceof String s) { formatted = String.format("String %s", s); } return formatted; } With pattern matching for switch the above code is reduced to static String formatterPatternSwitch(Object o) { return switch (o) { case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.toString(); }; } Furthermore, we can include the null test into the switch block and reduce boilerplate code for checking for null in a separate statement. The restrictions on the selector type have also been relaxed: the type of the selector expression can be either an integral primitive type or any reference type. All in all, pattern matching for switch enables the developers to write clear, concise code and reduces the risk of errors. Conclusion These features demonstrate that Java has an API for every occasion. Moreover, Java code becomes more user-friendly with every release. If you would like to experiment with the above solutions but your Java version doesn’t support them, download Liberica JDK — a TCK-verified OpenJDK distribution. And don’t worry about platform compatibility: Liberica works with the widest range of system configurations in the market! Download Liberica JDK - [CVE-2022-42889: a critical vulnerability in Apache Commons Text library](https://bell-sw.com/blog/cve-2022-42889-a-critical-vulnerability-in-apache-commons-text-library/): CVE-2022-42889, also known as “Text4Shell”, is a public vulnerability found in the Apache Commons Text library (commons-text-1.9.jar) versions 1.5 through 1.9 and included into the Apache dev list on October 13, 2022. Although the OpenJDK code is not affected by this vulnerability, we decided to publish a short notice since many Java developers use this library in production. Find out what it is about, whether your application is affected, and how to mitigate the risk of exploits. Description CVE-2022-42889 is nested in the StringSubstitutor class of the library, which performs variable interpolation. The standard interpolation format is “${prefix:name}”, where the prefix is a lookup performing interpolation. If the application uses some default lookups, namely “script”, “dns”, or “url”, they are interpolated by default due to a design flaw and allow for remote code execution or contact with untrusted remote servers when processing malicious input. Most requests are using the DNS prefix: a successful attempt will result in the victim site making a DNS query to the attacker-controlled listener domain. Risk scope Some compare Text4Shell to last year’s Log4Shell vulnerability as it affects an open-source library with a potentially wide extent of damage. But Apache Commons Text is less frequently used in an unsafe manner than Apache Log4j, so the probability of successful attacks is much lower. Nevertheless, the vulnerability was assigned a CVSS score of 9.8, which points to severe exploitability not extending beyond the vulnerable components. Mitigation The vulnerability was patched in Apache Commons Text version 1.10.0. It is recommended to upgrade the library as soon as possible to avoid any exploits. You should also check whether the framework you are using for your project utilizes the library. This is how you can check for CVE-2022-42889: Search for the library in .pom/.gradle file Use scanners to scan for this or any other vulnerability Analyze the libraries of the compiled project Run docker scan to check whether your container images contain the vulnerability - [Alpaquita Linux features explained](https://bell-sw.com/blog/alpaquita-linux-features-explained/): Alpaquita Linux won the performance race beating the strong opposition of popular Linux distributions for Cloud and Server. What makes Alpaquita so fast and adaptive to various performance challenges? Table of Contents Key enhancements Kernel Optimized libc Userspace Security hardening Conclusion Key enhancements Alpine served as a foundation for Alpaquita due to its size and performance. But it is based on musl libc, which can be inferior to glibc in some cases, and includes a malloc implementation not suitable for all workloads. Coupled with the lack of commercial support and LTS releases, Alpine is not always suitable for enterprise development despite being incredibly small and very popular. We removed obsolete components, optimized core configurations, and hardened the security features to boost Alpaquita’s performance and make it an optimal choice for enterprise. Below is the description of the most representative changes we introduced to our Linux. Kernel We balanced kernel options configuration for increased performance, including: NUMA (non-uniform memory access) options enable memory placement with NUMA aware scheduler for faster memory access Task group support (the CONFIG_RT_GROUP_SCHED option) enables the allocation of CPU bandwidth to realtime task groups. The allocated CPU time is dedicated to a realtime group performing a high-priority task, and the remaining CPU time is used for normal priority tasks, which helps to eliminate buffer underruns BFQ (Budget Fair Queueing), a proportional-share low-latency I/O scheduler (the CONFIG_IOSCHED_BFQ and CONFIG_BFQ_GROUP_IOSCHED options) provides high app responsiveness and distributes bandwidth, not just time, among processes. Therefore, it helps to reduce latency in the case of interactive and soft real-time (audio and video players/streamers) applications The CONFIG_NO_HZ_FULL option enables the reduction of scheduling-clock interrupts thus decreasing the OS jitter, which is important for high-performance computing and real-time apps, and improving energy efficiency In addition, Alpaquita Linux is based on the LTS kernel, 5.10 currently with planned support period up to December 2026. LTS versions are stable and most suitable for enterprise use so that the companies could upgrade the software at their own pace. Optimized libc The paragon of our effort is optimized musl libc library, musl perf. It outperforms the default musl libc and keeps up with the performance of glibc in some cases leaving it behind as well. To achieve this outstanding result, we performed the following optimizations: We integrated additional optimization options, -O2 (increases code performance) and -O3 (provides even better code performance optimization). -O3 is used for internal, malloc, and string subsystems and -O2 for other subsystems. Alpine, on the contrary, utilizes only the -Os option for all subsystems (optimizes for size) Contrary to Alpine, BellSoft’s musl supports indirect functions, which make it possible to select among multiple function implementations at runtime thus choosing the fastest one for a given processor. musl perf also supports various CPU-specific ASM functions (AVX512, EVEX, AVX2, SSE4, etc.) to boost the performance with a particular processor To benefit from new CPU instructions, the OS must be able to discover them. musl perf internally implements CPU features discovering Thanks to these and other optimizations, musl perf performance has proven to be equal or superior to that of glibc. But if a company is using a glibc-based Linux and unwilling to migrate to another libc, we provide a glibc-based version of Alpaquita. This way, our customers can benefit from all Alpaquita advantages and forget about migration issues. We also added three malloc implementations to the standard Alpine malloc for better performance with various workloads: rpmalloc is the smallest allocator (64K) with lock free thread caching. It is faster than other mallocs with less overhead in thread caches mimalloc (128K) is a small and consistent allocator with focus on performance. It is suitable for large scale low-latency services jemalloc (616K) is a general purpose allocator emphasizing fragmentation avoidance and scalable concurrency support. It natively supports threads with little memory fragmentation and provides scalability in multi-threaded systems Userspace In our article dedicated to Alpine Linux, we explained what makes it so small yet efficient, namely the selection of compact and performant utilities and modules. We adopted similar mindset with Alpaquita and implemented the following userspace options: Busybox is a set of command-line utilities of only 1 MB in size. It contains a set of 400 Unix utilities thus providing a small yet complete environment for system maintenance. In addition, developers can add or remove components as needed. For those who require a more sophisticated set of command-line options, we provide coreutils on-demand apk (Alpine Package Keeper) is a small package manager providing additional size optimization OpenRC is an init system without any unnecessary features, which makes it smaller than other Linux init systems, but efficient There are no graphics to keep the system size down In addition, Alpaquita Linux is compatible with Docker. In fact, one of its habitats is BellSoft’s profile on Docker Hub, where you can select from a variety of ready-to-go container images. It also supports QEMU, which is used for emulation and virtualization. Security hardening Excellent security is just as important for enterprise development as performance. We added enterprise-grade security features to Alpaquita Linux that, coupled with low attack surface, guarantee maximum protection, including but not limited to: Kernel lockdown including early in boot to prevent both direct and indirect access to the running kernel image Support for Secure Boot, an UEFI firmware security protocol validating the authenticity of the loaded code and thus ensuring that only immutable and signed software components are loaded during the boot time. Find out how to set up Secure Boot in a dedicated guide. Kernel module signing with SHA-512 forbids the loading of unsigned modules or modules signed with an invalid key LTS releases and security updates based on the strict schedule help to keep the OS safe at all times Security advisory (underway) will keep the users informed about discovered issues and available patches Commercial 24/7 support with timely fixes and patches Conclusion These and other optimizations made Alpaquita Linux fast, secure, and incredibly flexible helping it to overcome hurdles that make other Linux distros jib. Try it out and see it for yourself! Download Alpaquita Linux - [Java microcontainers with MicroProfile, jlink, and Liberica JDK](https://bell-sw.com/blog/creating-java-microcontainers-with-microprofile-jlink-and-liberica-jdk/): With 81% of companies having a multi-cloud strategy planned or in the works, and 67% of corporate infrastructure being cloud-based, cloud computing has become a new norm. But everything comes at a price, and cloud resources are no exception. Despite minuscule prices for machines and cloud capacities, cloud bills can be fifty pages long. Why? One of the main reasons is heavy underperforming containers. They devour time, memory, and company’s money. The solution is to minimize the size of containers and at the same time optimize the performance. In this article, we will learn how to do that by building Java microservices with Liberica JDK and MicroProfile and creating microcontainers with jlink. Prepare your microscope, we are talking about tiny numbers here! Table of Contents Create a Java application with MicroProfile Brief introduction to MicroProfile Build a Java microservice with MicroProfile Create a microcontainer using jlink Conclusion Create a Java application with MicroProfile Brief introduction to MicroProfile MicroProfile is an open-source specification for building scalable and secure Java microservices. MicroProfile rests upon Jakarta EE standards, so it allows you to develop microservices without the need to define key components from scratch. In addition, MicroProfile evolves rapidly, which enables companies to take advantage of the newest technologies as soon as possible. And the open-source nature of MicroProfile eliminates vendor lock-in and makes it possible to create microservices using both MicroProfile and Jakarta EE features. Moreover, due to the loosely-coupled nature of microservices, you can develop them using different frameworks — MicroProfile, Spring, etc. Your opportunities are limitless. Let’s get down to business and create a microservice using this robust tool! Build a Java microservice with MicroProfile To create and run our demo application, we will be using Liberica JDK, a progressive open-source Java runtime. Liberica JDK simplifies the creation and maintenance of microservices as it supports the widest range of platforms and configurations, including the Apple Silicon. Moreover, it is developed by a top-5 OpenJDK enterprise contributor and a member of the OpenJDK Vulnerability Group, so your applications are safe and sound at all times. Discover Liberica JDK We will use the latest release of LTS Liberica JDK 17 for our project due to the jdeps bug, which was fixed in this version. You can download Liberica JDK 17 directly from BellSoft’s website or through a package manager. Let’s start with building a simple Quarkus application with Maven. Quarkus is the MicroProfile implementation perfect for building Java microservices. It allows the developers to use the existing enterprise APIs and adjust them to their purposes. We have already seen Quarkus in action when we built native images with Liberica Native Image Kit. This time, the framework will help us explore MicroProfile. In the Terminal, navigate to the folder you want to create your project in and run this command: mvn io.quarkus.platform:quarkus-maven-plugin:2.7.3.Final:create \ -DprojectGroupId=mpdemo \ -DprojectArtifactId=jrushmp Open the newly created application in your favorite IDE. If you look closely at the pom.xml file, you will see the dependencies for Quarkus, but not MicroProfile, so we need to add them manually: io.quarkus quarkus-smallrye-openapi io.quarkus quarkus-smallrye-opentracing io.quarkus quarkus-smallrye-fault-tolerance io.quarkus quarkus-smallrye-health io.quarkus quarkus-smallrye-metrics io.quarkus quarkus-resteasy-jsonb io.quarkus quarkus-rest-client Navigate to the folder containing the pom.xml (in our case, it is jrushmp) and run mvn compile quarkus:dev -Dquarkus.test.continuous-testing=disabled This command will compile the service. Run mvn package And then java -jar target/quarkus-app/quarkus-run.jar As a result, you will get a loaded application with all the standard APIs. You can now enhance and personalize your microservice. Go to the GreetingsResource class of your application and add the following code: package mpdemo; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.metrics.annotation.Metered; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/hello") public class GreetingResource { @Inject @ConfigProperty(name = "message", defaultValue = "Hello from MicroProfile!") String message; @GET @Retry @Metered @Produces(MediaType.TEXT_PLAIN) public String hello() { return this.message; } } Note that the @Metered annotation enables you to track throughput/frequency data. Run this again: mvn package quarkus:dev -Dmaven.test.skip=true -Dquarkus.test.continuous-testing=disabled Then you can check the correctness of the output by running curl localhost:8080/hello It should give you Hello from MicroProfile! You can input the following command to get the metrics of your application: curl localhost:8080/q/metrics/application That’s it! You now have a working Java microservice with MicroProfile APIs. You can create an application with MicroProfile only, and without being bound to a specific framework. This app will then work with any technology that implements MicroProfile. However, we used Quarkus to demonstrate that you can keep to the Jakarta EE standards and at the same time take advantage of the novelties offered by modern tools. We can now proceed to containerizing our application and sending it to the cloud. Create a microcontainer using jlink We will now pack our app into a container and ship it to the cloud. Normally, we would create a Docker image by simply running ./mvnw package and docker build -f src/main/docker/Dockerfile.jvm -t quarkus/jrushmp-jvm . However, a standard Docker image is heavy because it includes the whole Java Development Kit. For example, the size of our application containerized this way is approx. 457MB (the actual size may vary). But we can drastically reduce the size of our image by modularizing the microservice. Modularization is the process of dividing an application into modules, i.e. only necessary classes and dependencies get bundled. This means that you get a custom trimmed-down JRE in your container, thus minimizing its size and increasing the performance. And jlink is the tool made for cutting out a custom Java runtime image from a standard JDK by leaving only necessary modules for your application. Let’s see it in action! First, we need to turn our application into a module system. For that purpose, we need to add several dependencies to the pom.xml file: To begin with, we add a configuration for building an uber-jar: true We also add the maven-dependency-plugin, which collects all jar files into one directory (in our case, target/lib). org.apache.maven.plugins maven-dependency-plugin 3.1.1 copy package copy-dependencies ${project.build.directory}/lib jboss-jaxb-api_2.3_spec Finally, we exclude the tests because we don’t need them for our purposes. io.quarkus quarkus-test-common Note that you will need to add the org.jboss.spec.javax.xml.bind jboss-jaxb-api_2.3_spec section if you get the Error: Two versions of module java.xml.bind. In this case, we exclude the external JBoss API and use Jakarta EE API. Remember, you can use the mvn-jlink plugin to execute tools in the JDK/bin folder. If the plugin needs to make an image of a specific JDK, it downloads the required JDK distro from a provider, Liberica JDK among others. The next step is to create an uber-jar. Don’t forget that you need to use the latest Maven version to perform this action with JDK 17. From the project directory, run JAVA_HOME="/home/User/java/jdk-17.0.4.1" ~/apache-maven-3.8.5/bin/mvn package -Dquarkus.package.type=uber-jar Now, we need to use jdeps to analyze the modules we need with the following command ~/java/jdk-17.0.4.1/bin/jdeps --multi-release 11 -cp target/lib/*:target/quarkus-app/lib/boot/*:target/quarkus-app/lib/* --ignore-missing-deps --list-deps target/jrushmp-1.0.0-SNAPSHOT-runner.jar which will give you the list of modules the application needs. The list of modules JDK removed internal API/com.sun.tools.javac.code java.base/sun.security.x509 java.compiler java.datatransfer java.desktop java.logging java.management java.naming java.rmi java.security.jgss java.security.sasl java.sql java.transaction.xa java.xml jdk.compiler/com.sun.tools.javac.code jdk.compiler/com.sun.tools.javac.tree jdk.compiler/com.sun.tools.javac.util jdk.management jdk.unsupported Note that the jdeps command will be slightly different for macOS users: ~/java/jdk-17.0.4.1/bin/jdeps --multi-release 11 -cp "target/lib/*:target/quarkus-app/lib/boot/*:target/quarkus-app/lib/*" --ignore-missing-deps --list-deps target/jrushmp-1.0.0-SNAPSHOT-runner.jar Finally, let’s use jlink to cut out a custom JRE: ~/java/jdk-17.0.4.1/bin/jlink --compress 2 --strip-debug --no-header-files --no-man-pages --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.logging,java.management,java.naming,java.rmi,java.security.sasl,java.security.jgss,java.sql,java.transaction.xa,java.xml,jdk.compiler,jdk.management,jdk.unsupported,jdk.zipfs --output target/jlink-runtime The size of jlink-runtime is 67MB. To put the application into a container, we need a Dockerfile to build an image with jlink and custom JRE. We will use Liberica Runtime Container based on Liberica Lite and Alpaquita Linux, BellSoft’s lightweight Linux distro with remarkable performance characteristics. Dockerfile for jlink app image FROM bellsoft/liberica-runtime-container:jdk-all-17-musl as builder # Create custom JRE RUN jlink --compress 2 --strip-java-debug-attributes --no-header-files --no-man-pages --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.logging,java.management,java.naming,java.rmi,java.security.sasl,java.security.jgss,java.sql,java.transaction.xa,java.xml,jdk.compiler,jdk.management,jdk.unsupported,jdk.zipfs --output /jlink-runtime FROM bellsoft/alpaquita-linux-base:stream-musl COPY --from=builder /jlink-runtime /jlink-runtime COPY target/quarkus-app/lib/ /opt/quarkus-app/lib/ COPY target/quarkus-app/*.jar /opt/quarkus-app/ COPY target/quarkus-app/app/ /opt/quarkus-app/app/ COPY target/quarkus-app/quarkus/ /opt/quarkus-app/quarkus/ EXPOSE 8080 ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" ENTRYPOINT ["/jlink-runtime/bin/java", "-jar", "/opt/quarkus-app/quarkus-run.jar"] Copy the file into your project. You can now build a Docker image and run it: docker build -f Dockerfile -t jlink:1.0 . docker run -it --rm -p 8080:8080 jlink:1.0 If the COPY target/jlink-runtime/ /jlink-runtime/ command fails with “file not found error”, add !target/jlink-runtime/* to the end of the file. Check your Docker images by running docker images. The container size is 109MB, which is 4 times less than a Docker container image built by default! Conclusion In this article, we discovered the power of the jlink tool when it comes to creating small but powerful containers with Liberica JDK. Such containers require four times less cloud resources but don’t affect the performance of your application. Great news is that you can now build your own microcontainers. Or use BellSoft’s ready-made solution for your application — Liberica Runtime Container. Choose the package that suits your needs, put your app into the container and you are good to go! Discover Liberica Runtime Container The article was inspired by Adam Bien's talk at JRush in December, where he explained how to use MicroProfile to built resilient Java microservices. Head over to the JRush page to listen to the full presentation. - [Alpaquita vs Alpine: a head-to-head comparison](https://bell-sw.com/blog/alpaquita-vs-alpine-a-head-to-head-comparison/): You need a lightweight Linux distribution if you want to drastically reduce the size of your containers. You might have already done your research and narrowed the choice down to Alpine and Alpaquita Linux. Both are incredibly small, performant, and secure. But there are certain differences that may influence your decision. Find a clear and concise comparison of both Linux distributions below. Table of Contents Differences between Alpaquita and Alpine Developer Software licensing Release cycle Support C library implementation Platform compatibility Conclusion Differences between Alpaquita and Alpine Developer Alpaquita Alpine BellSoft Alpine Linux development team Alpine Linux was first introduced in 2005 and has been developed within the Alpine Linux project as a non-commercial OS since then. The idea behind Alpine was to create a lightweight, simple and secure Linux distribution to be used within containers together with server and cloud applications. Alpaquita Linux enhances and evolves the way this task is executed. Alpaquita was developed by BellSoft, a leading OpenJDK contributor, and introduced in 2022. BellSoft engineers have always appreciated Alpine for its size and performance, so in 2020 they created the smallest containers on the market at that time with Alpine and Liberica Lite. Alpaquita Linux was designed to be just as small as Alpine, and also to include features for enhanced performance and security, packages for Java development, and commercial support. Thus, Alpaquita became the only OS with Alpine’s features that lacks its disadvantages and is fit for enterprise cloud apps. Software licensing Alpaquita Alpine EULA GPL2 mostly MIT for muslVarious licenses for software components Alpine Linux is built around the Linux kernel, which is distributed under the GPL license. Core utilities such as Busybox and apk-tools are also distributed under GPLv2, whereas musl is subject to the MIT license. As with any open-source technology, Alpine Linux contains packages under various licenses: a more detailed package license information can be found here. Alpaquita Linux is distributed under EULA (End-user license agreement). It means that we have verified all the packages regarding clean licenses and substituted some software to eliminate the risk of license violation. Therefore, Alpaquita is legally safe to use in enterprises and an unexpected license change is not an issue you have to worry about. Release cycle Alpaquita Alpine LTS releases aligned with Linux Kernel LTS Rolling releases of Alpaquita Stream Rolling release (Alpine Edge) Stable releases each May and November The Alpine Linux team provides two types of releases: edge and stable. Edge is a rolling release with the latest packages and updates. Stable releases see the light each May and November and are usually supported for two years. Alpaquita Linux has Stream and LTS versions. Alpaquita Stream is similar to Alpine Edge, with the freshest improvements and updates. LTS releases are stable versions supported for four years with two-years overlap with the previous version. Both Alpine and Alpaquita receive security patches as soon as they become available. Support Alpaquita Alpine Community support for Alpaquita StreamCommercial support for LTS releases Community support only Alpine Linux is a free, non-commercial distribution. Community members work together to provide enhancements, fixes, and security patches as soon as possible. In addition, anybody can participate in the process of Alpine improvement. But to report a bug, a user can open a new issue on a bugtracker or email the team directly, and there is no fixed timeline for issue resolution. Commercial Linux support is extremely important for enterprises working in the cloud or server environment. Therefore, LTS releases of Alpaquita come with commercial support from BellSoft, with 24/7 service, security advisory, and prompt fixes based on SLA. In addition, BellSoft provides Alpaquita Cloud Native Platform — a solution with Alpaquita, a unified Java runtime Liberica Lite, and Liberica Native Image Kit for native images generation, with support for Linux and Java from one vendor. This way, you can unify the technology stack, minimize the number of vendors, and keep your software secure and performant at all times. C library implementation Alpaquita Alpine musl def musl perf glibc musl def One of the reasons Alpine Linux is so lightweight is the C library implementation it utilizes — musl. musl has certain benefits over a more popular glibc: it has a cleaner code base, smaller overhead and attack surface. Differences between musl and glibc performance are usually negligible, although musl can demonstrate inferior results in some cases. In addition, migration to another libc is complicated and associated with compatibility issues. Alpaquita Linux comes in two flavors. One is based on musl perf — an optimized musl created by BellSoft engineers with impressive performance characteristics: it outran musl def and even glibc in a series of benchmarks. The second Alpaquita version is based on glibc and is optimal for companies who want to take advantage of microcontainers and avoid migration issues. Learn more about musl and glibc specifics We also added several features to boost Alpaquita’s performance even more — find out more in the corresponding article. And if you aim for higher availability and optimized cloud costs, try Alpaquita Containers with Coordinated Restore at Checkpoint support: CRaC API helps you to reduce Java application startup to mere milliseconds and otimize cloud resorces consumption. Platform compatibility Alpaquita Alpine x86_64 x86 x86_64 ARMhf ARMv7 AArch64 ppc64le s390x Alpine Linux supports a wide variety of CPU architectures, including the most popular x86 and x86_64 (AMD64) and non-x86 architectures, such as ARM or PowerPC. As far as Docker images are concerned, Alpine is available only as a base image without additional tools. Alpaquita Linux currently supports x86_64 only. The support for other architectures will be gradually added in the following releases. At the same time, we deliver a wide variety of Alpaquita Docker images, including containers with Liberica JDK, Liberica NIK, and images for GCC and Python. Visit BellSoft’s Docker Hub repositories Conclusion Both Alpine and Alpaquita are minimalistic, performant, and secure. If you want to have a free lightweight distribution and receive help from the community, Alpine is an optimal choice. But if you would like to Increase the performance of your applications Receive prompt support from the vendor Be sure of 100% license compliance Utilize optimized musl or glibc Take a closer look at BellSoft’s Linux. No need to take our word for it — give it a ride and see for yourself the beauty that is Alpaquita! Discover Alpaquita - [Critical vulnerabilities in OpenSSL 3.0](https://bell-sw.com/blog/critical-vulnerabilities-in-openssl-3-0/): The OpenSSL Project released a Security Advisory on November 1, 2022, concerning two critical vulnerabilities discovered in the OpenSSL library versions 3.0.0 to 3.0.6. OpenJDK distributions, including Liberica JDK, use their own implementation of TLS and therefore are not affected. But OpenSSL is a very popular library, and is very likely implemented in the software you are using in your project. So read on to find more about the risks and possible solutions. What is the issue? Both vulnerabilities were assigned a High severity level. Both can be triggered during a client’s or server's validation of an X.509 certificate. With the first one, X.509 Email Address 4-byte Buffer Overflow (CVE-2022-3602), a specifically crafted email address can overflow four attacker-controlled bytes on the stack. In the case of the second vulnerability, X.509 Email Address Variable Length Buffer Overflow (CVE-2022-3786), a buffer overflow can be caused by a malicious email address abusing an arbitrary number of bytes containing the “.” character (decimal 46) on the stack. The CVEs’ exploitation may lead to denial of service (DoS) or remote code execution (RCE). Both CVEs can be triggered if a vulnerable TLS client connects to a malicious server or a vulnerable TLS server requests client authentication and a malicious client connects. What should you do? The CVEs were patched in version 3.0.7. If you are using OpenSSL 3.0, you should upgrade to the newest library version as soon as possible: this is the only way to deal with CVE-2022-3602. In the case of CVE-2022-3786, you can temporarily disable the verification of client certificates. Library versions 1.1.1 and 1.0.2 are not affected by the issue. If the library is bundled with the third-party software you are using, you should update the software as soon as the patch becomes available. This is also the case with operating systems with OpenSSL installed (Ubuntu 22.04, CentOS Stream 9, Alpine Edge, etc.). Also, Amazon Linux 1 and Amazon Linux 2 don’t ship with OpenSSL 3.0, so no patch is required. As far as Alpine Linux is concerned, the patch is already available. And of course we keep BelSoft software safe. If you are using our containers based on Liberica JDK and these distributions, you don’t have to worry because they do not include this library by default. To keep Alpaquita Linux secure, we have already updated its OpenSSL package. You can find the patched version in Alpaquita's repositories. Conclusion Keeping to the latest software version helps to guard against CVEs and other issues. To find out more about previous critical vulnerabilities in outdated security protocols, read our article End of life for old TLS. Stay safe! - [Java microservices communication with gRPC and Liberica JDK](https://bell-sw.com/blog/highly-performant-java-microservices-communication-with-grpc-and-liberica-jdk/): In modern cloud-native software development, microservices are arguably the most widely used architectural pattern. To learn more about it, read the previous article Microservices 101: Understanding the architecture. If you want to dive into the microservice architecture design patterns, you can read my article Microservice Architecture and its 10 Most Important Design Patterns. In a microservice architecture, a smaller service focuses on a particular domain, so the cognitive load for developing and maintaining the service is low. But the services must communicate to fulfill business requirements. This article will show how to establish high-performance microservice communication using gRPC (a Remote Procedure Call framework) and Liberica JDK. Table of Contents Implementation Install Java Create a project Define the .proto file Generate the Java code Define the server Define the client Performance comparison between gRPC and REST Use cases Alternatives in the JVM world Conclusion Implementation Microservices can communicate with each other synchronously and asynchronously. REST is extensively used for synchronous communication, but gRPC is also gaining popularity. Like REST, gRPC provides a cross-platform language-agnostic way for services to communicate. In 2016, Google created gRPC to overcome the limitations of REST for microservice communication using RPC (Remote Procedure Call). It is based on HTTP/2 and uses the binary format Protobuf for better performance. In performance-critical applications, gRPC can give an edge over REST as it is more efficient. Due to gRPC’s use of HTTP/2 and binary message format Protobuf, gRPC edges out REST for the following three cases: higher performance bi-directional streaming client-side load balancing For our Demo, we will develop a simple Payment microservice that can create a payment similar to PayPal. For the sake of simplicity, this Payment microservice will be transient, i.e., without any database. System requirements: Java 17 Gradle Protocol buffer compiler Install Java Install Liberica JDK 17 (the latest LTS Java release). To verify the installation, run the following command: java –version You will get the following output: openjdk 17.0.4.1 2022-08-12 LTS OpenJDK runtime environment (build 17.0.4.1+1-LTS) OpenJDK 64-Bit server VM (build 17.0.4.1+1-LTS, mixed mode, sharing) As mentioned earlier, the gRPC usually uses protocol buffer as a service definition and messaging format. In gRPC, the service and message definitions are set in the .proto files. The protocol buffer compiler converts the .proto file into a language-specific implementation. As a prerequisite, we need to install the protocol buffer compiler on our machine, as described in the official documentation. Here is the command to install protocol buffer in Ubuntu system: sudo apt install -y protobuf-compiler If successfully installed, we can check the protocol buffer compiler version using the command: protoc –version The output will be: libprotoc 3.12.4 Create a project In this Demo, we will create a Gradle project with IDE (e.g., IntelliJ) using Java 17. We need to add the following dependencies for the gRPC and protocol buffer support: implementation 'io.grpc:grpc-netty:1.49.0' implementation 'io.grpc:grpc-protobuf:1.49.0' implementation 'io.grpc:grpc-stub:1.49.0' Define the .proto file gRPC is a contract-first communication system where the service contract between the client and server must first be defined. The Java source code will be generated from the .proto files. Here is the definition of the .proto file for our Payment microservice: .proto file syntax = "proto3"; option java_multiple_files = true; package org.mkzaman.grpcservice; import "google/protobuf/timestamp.proto"; message Person { int32 id=1; string name = 2; string email = 3; } message PaymentRequest { Person sender = 1; Person receiver = 2; string purpose = 3; double amount = 4; } enum PaymentStatus { SUCCESS = 0; FAILURE = 1; } message PaymentResponse { PaymentStatus status = 1; string paymentId = 2; google.protobuf.Timestamp executionTime = 3; } service PaymentService { rpc sendPayment(PaymentRequest) returns (PaymentResponse); } Let’s analyze the file line by line. The .proto file starts with the following basic configuration: syntax = "proto3"; option java_multiple_files = true; package org.mkzaman.grpcservice; import "google/protobuf/timestamp.proto"; The first line defines the Protobuf version we are using, which is version 3. The second line states that multiple Java files will be generated from the .proto file. The third line defines the package name of the generated Java files. In the .proto file, we can also import other .proto files, including their definition, using “import.” In the fourth line, we import Google’s “timestamp.proto” file to use the Standard timestamp attribute. Here is the message format: message format message Person { int32 id=1; string name = 2; string email = 3; } message PaymentRequest { Person sender = 1; Person receiver = 2; string purpose = 3; double amount = 4; } enum PaymentStatus { SUCCESS = 0; FAILURE = 1; } message PaymentResponse { PaymentStatus paymentStatus = 1; string paymentId = 2; google.protobuf.Timestamp paymentTime = 3; } We defined the “Person” message in the first few lines. We need to give numbers for each parameter. Unlike REST, where attribute name (e.g., “id”) is passed every time, Protobuf passes the number “1” instead. We also defined the PaymentRequest containing the sender, receiver, purpose, and amount. Similarly, we defined the PaymentResponse containing the paymentStatus, paymentId, and paymentTime. Finally, here is the Payment service contract: service PaymentService { rpc sendPayment(PaymentRequest) returns (PaymentResponse); } The service contains a simple sendPayment method that accepts a PaymentRequest and returns a PaymentResponse. Generate the Java code There are several ways to generate the Java code from the .proto contract. One way is to install the protocol buffer compiler “protoc” in our local machine as described in the gRPC documentation. This compiler “protoc” can then be used to generate the Java code from the .proto file. The other way is to use the Gradle plugin to generate the Java code from the .proto file. We will use the Gradle plugin for that. Please visit the official website to learn more about the gRPC Gradle plugin. First, save the “service.proto” file in the “src/main/proto” directory. Then, add the protocol buffer Gradle plugin in the build.gradle file as given below: plugins { id 'java' id 'com.google.protobuf' version '0.8.19' } We also need to configure the Protobuf compilation in the following way: protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.12.4' } plugins { grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.49.0' } } generateProtoTasks { all()*.plugins { grpc {} } } } Finally, add the generated source code as “sourceSets” so that IDE can find and link the generated Java codes. After the successful Gradle build, the following message files are generated: PaymentRequest.java PaymentResponse.java PaymentStatus.java Person.java The PaymentServiceGrpc.java Service file is also generated. It contains the static abstract class PaymentServiceImplBase that includes the stub method sendPayment(). We need to extend the PaymentServiceImplBase class to implement the sendPayment() method. Define the server Define the class PaymentServiceImpl and fulfill the sendPayment() method as shown below: public void sendPayment(PaymentRequest request, StreamObserver responseObserver) { Instant time = Instant.now(); Timestamp timestamp = Timestamp.newBuilder().setSeconds(time.getEpochSecond()) .setNanos(time.getNano()).build(); PaymentResponse response = PaymentResponse.newBuilder() .setPaymentId(UUID.randomUUID().toString()) .setPaymentTime(timestamp) .setPaymentStatus(PaymentStatus.SUCCESS) .build(); responseObserver.onNext(response); responseObserver.onCompleted(); logger.info("Payment request = " + request); logger.info("Payment response = " + response); } The PaymentResponse is generated with the SUCCESS status, Payment ID, and Payment Time. The gRPC Server is then defined with the following method: public static void main(String[] args) throws IOException, InterruptedException { Server server = ServerBuilder .forPort(8080) .addService(new PaymentServiceImpl()).build(); server.start(); server.awaitTermination(); } The gRPC Server starts listening on port 8080 with the already defined Payment service implementation. Define the client The gRPC client is defined with the following main() method: public static void main(String[] args) { ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080) .usePlaintext() .build(); PaymentServiceGrpc.PaymentServiceBlockingStub blockingStub = PaymentServiceGrpc.newBlockingStub(channel); PaymentRequest paymentRequest = PaymentRequest.newBuilder() .setSender(Person.newBuilder().setName("Alice").setId(1).setEmail("alice@alice.com").build()) .setReceiver(Person.newBuilder().setName("Bob").setId(2).setEmail("bob@bob.com").build()) .setPurpose("Private") .setAmount(1000.00) .build(); PaymentResponse paymentResponse = blockingStub.sendPayment(paymentRequest); channel.shutdown(); } To abstract away the low-level details of the gRPC connection (like connection, connection pooling, and load balancing), gRPC provides the high-level ManagedChannel. In this case, we specify the gRPC server address. In addition, the channel is defined as “plaintext,” i.e., without encryption. The stub PaymentServiceBlockingStub is used to make the actual remote method call sendPayment(). The client uses the stub to interact with the server. Here, we are using a BlockingStub, which will wait until the response is received. There is also the PaymentServiceStub (an asynchronous stub) and PaymentServiceFutureStub (a future stub) for asynchronous communication with the server. A simple PaymentRequest is created to test the gRPC communication between the client and server. Once the remote sendPayment() method is called synchronously using the stub, a PaymentResponse is obtained. Here is the generated log message: 2022-09-14 00:54:59 INFO PaymentServiceImpl:31 - Payment request = sender { id: 1 name: "Alice" email: "alice@alice.com" } receiver { id: 2 name: "Bob" email: "bob@bob.com" } purpose: "Private" amount: 1000.0 2022-09-14 00:54:59 INFO PaymentServiceImpl:32 - Payment response = paymentId: "23490a4d-75cc-49c0-9012-bc203624f576" paymentTime { seconds: 1663109699 nanos: 247148687 } The log message shows that the gRPC client and server have communicated synchronously using the predefined service.proto file. Performance comparison between gRPC and REST One of the main reasons to choose gRPC over REST is gRPC’s superior performance, the key driver for which is the Protobuf binary format used instead of JSON text format. Let us compare the size of the serialized Protobuf message and the JSON message using the library: protobuf-java-util. Below is the code snippet used for the comparison: final int serializedSize = paymentRequest.getSerializedSize(); System.out.println("serializedSize = " + serializedSize); final String jsonMessage = JsonFormat.printer().print(paymentRequest); System.out.println("jsonMessageSize = " + jsonMessage.length()); In our example, the Protobuf binary message size for PaymentRequest is 68 bytes, and the JSON message size is 210 bytes. That means Protobuf message size for PaymentRequest is ca. 32% of the equivalent JSON message. In the article Evaluating Performance of REST vs. gRPC, the author compared gRPC and REST by sending plaintext JSON requests over HTTP. According to that benchmark, gRPC is roughly seven times faster than REST when receiving data and approx. ten times faster than REST when sending data for this specific payload. In another article, gRPC vs REST Performance Comparison, the author conducted a similar performance test by sending plaintext requests over HTTP in both the unidirectional and bidirectional way. The unidirectional communication results clearly show that gRPC performs better than REST in terms of CPU utilization, throughput, and response time: CPU Utilization Throughput (Requests/Second) 50th Percentile Response Time 90th Percentile Response Time REST ~85% 15.26 6.451 seconds 6.823 seconds gRPC ~52% 37.29 2.442 seconds 3.381 seconds Source: gRPC vs REST Performance Comparison For bidirectional communication, gRPC demonstrates even better performance: CPU Utilization Throughput (Requests/Second) 50th Percentile Response Time 90th Percentile Response Time REST ~85% 15.26 6.451 seconds 6.823 seconds gRPC-Unary ~52% 37.29 2.442 seconds 3.381 seconds gRPC Bi-Directional Stream ~42% 94.98 1.042 seconds 1.148 seconds Use cases Although gRPC can be used instead of REST in all cases, I suggest using it in situations where performance, throughput, latency, and CPU utilization are the key performance indicators. Therefore, I would recommend gRPC for the following: Bi-directional streaming Highly performant microservice communication IoT High-performance streaming (e.g., Kafka or any other message bus and message queue). Moreover, most public cloud services (e.g., API Gateway, message bus, message queue) natively support gRPC. Alternatives in the JVM world Suppose all microservices within a project are developed using JVM languages (e.g., Java, Scala, Kotlin, etc.). In that case, a pure JVM library can be used for client-server communication over TCP or UDP, or RMI-based RPC communication. One such library is KryoNet, which uses a popular and efficient binary object graph serialization framework Kryo. Although Kryo is a good serialization framework and provides an alternative to Protobuf for RPC, it is not always more efficient than Protobuf and should be evaluated on a use-case basis. Here is the code snippet to compare the serialized message size: Kryo kryo = new Kryo(); kryo.register(PaymentRequest.class); ByteArrayOutputStream baos = new ByteArrayOutputStream(); Output output = new Output(baos); kryo.writeObject(output, paymentRequest); output.close(); final byte[] bytes = baos.toByteArray(); System.out.println("bytes.length = " + bytes.length); The serialized message size is 98. Although it is much smaller than JSON (46%), it is still comparatively larger than Protobuf (32%). Conclusion In this article, we mastered highly performant microservice communication with gRPC. There may be a better technology for typical web client and backend microservice communication. Still, gRPC can be preferred to REST for microservice-to-microservice communication thanks to its efficient features and excellent performance. In addition, gRPC is the go-to protocol if we need bi-directional streaming between microservices. We demonstrated a straightforward way to establish the gRPC-based microservice communication using Java and Gradle. As you can see, Java supports modern RPC frameworks such as gRPC and is as performant and multifunctional as any other modern programming language. The source code of the project is available on GitHub. - [Deploy application replicas in local Kubernetes clusters](https://bell-sw.com/blog/how-to-deploy-application-replicas-in-local-kubernetes-clusters/): We continue our series on optimizing Java applications on Kubernetes. We learned how to create a local single-node Kubernetes cluster in the first part. This time, we will look into deploying multiple application replicas in it. This deployment strategy is helpful if you need to study pod sizing and application scalability, considering the JVM flags and resource limits. Table of Contents Set up the environment Deploy an application Conclusion Set up the environment We assume you already have a running local K8s cluster built with minikube, an easy-to-install Kubernetes distribution for local development and testing. Otherwise, refer to the tutorial. We use an x86_64 machine running Linux, with 96 CPUs and a lot of RAM. We will also require the following technologies: Docker, a container runtime, which we will use as a driver because there’s no need for virtualization NGINX Ingress controller for traffic distribution between application replicas Metrics Server, a reference implementation of the Metrics API for resource metrics collection and aggregation. We will use it instead of Heapster marked as deprecated in the newer K8s versions First, let’s set up a driver and assign 88 CPUs and 32 GB to the cluster under the load. Install Docker 18.09 or higher and ensure the Docker daemon is running with $ sudo systemctl start docker Start a minikube cluster with the following command: $ minikube start --driver=docker --extra-config=kubelet.housekeeping-interval=10s --cpus 88 --memory 32768 The command above solves two issues. The first one may appear when checking the pod metrics with metrics-server and is solved by adding a metrics interval. The second issue is caused by unresponsive minikube and is eliminated by changing the number of CPUs and memory allocated to it (it uses 2 CPUs and 2048 MB by default). To make docker the default driver, run $ minikube config set driver docker Now, we need to enable Ingress and Metrics Server, which are provided as minikube addons (K8s extensions). The commands are as follows: $ minikube addons enable ingress If you are using Kubernetes version older than 1.11, you must disable Heapster first. $ minikube addons disable heapster $ minikube addons enable metrics-server Now that you are all set, you can check the cluster information by running $ kubectl cluster-info Deploy an application We will use a containerized Spring Boot Petclinic application as a demo. Refer to the tutorial on dockerizing a Spring Boot app to build a container. The following steps concern the deployment, as well as service and Ingress rules configuration. We want to use a local Docker image for local testing instead of pulling one from a Docker registry. But minikube doesn’t see the local images and pulls the images from private or public repositories by default. The reason is that it uses its own Docker environment, and its Docker environment is different from the one running on our machine. To solve the issue, we can export the app container image from the local repository, switch to minikube’s shell, and load it there. Load the image from your local Docker environment into Minikube’s cluster environment: $ minikube image load petclinic-jpa-jvm:0.0.1-SNAPSHOT Other options are to use minikube image build to build the image in the cluster environment, or to export the image as a file in the local environment and import it in Docker in minikube’s environment. Switch to minikube’s Docker in a current shell session. It makes sense to keep regular and minikube sessions in parallel. $ eval $(minikube docker-env) Now we can configure the deployment, service, and Ingress. We need three configuration files for that purpose: 01-deployment-jvm1.yaml 02-service.yaml 03-ingress.yaml In 01-deployment-jvm1.yaml, we describe: Which container to deploy Minimal pod requirements to be able to run a replica — CPU and memory requests Maximum pod resources to provide to a replica — CPU and memory limits Application configuration (environment). For simplicity’s sake, we make each replica use its own in-memory database How many replicas to have. We’ll have a single replica but you can easily vary the number. How we refer that deployed application (selector) 01-deployment-jvm1.yaml apiVersion: apps/v1 kind: Deployment metadata: name: petclinic spec: selector: matchLabels: app: petclinic replicas: 1 template: metadata: labels: app: petclinic spec: containers: - name: petclinic image: petclinic-jpa-jvm:0.0.1-SNAPSHOT imagePullPolicy: Never ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "hsqldb" - name: JAVA_TOOL_OPTIONS value: "-XX:ActiveProcessorCount=8" resources: limits: cpu: "8" memory: 16Gi requests: cpu: "8" memory: 16Gi args: - -cpus - "8" In 02-service.yaml, we state that Petclinic replicas can communicate on port 8080 02-service.yaml kind: Service apiVersion: v1 metadata: name: petclinic spec: selector: app: petclinic ports: - name: http port: 8080 In 03-ingress.yaml, we expose a single 8080 endpoint for external connections to the cluster that are backed by all Petclinic replicas. 03-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: petclinic spec: defaultBackend: service: name: petclinic port: number: 8080 Apply all configuration files with the following commands: $ kubectl apply -f 01-deployment-jvm1.yaml $ kubectl apply -f 02-service.yaml $ kubectl apply -f 03-ingress.yaml External connections can be made by retrieving the IP address of a node: $ minikube ip You can now observe your running cluster: $ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE petclinic-jvm1 1/1 1 1 6s $ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 443/TCP 14h petclinic-jvm1 ClusterIP 10.98.212.162 8080/TCP 11s $ kubectl get ingresses NAME CLASS HOSTS ADDRESS PORTS AGE petclinic-jvm1 nginx * 192.168.49.2 80 10s Or $ kubectl get pods NAME READY STATUS RESTARTS AGE petclinic-54d8b6b5-5fzrh 1/1 Running 2 (18m ago) 161d You can find more details with commands such as $ kubectl describe nodes $ kubectl describe pods etc. After you found your pod’s name, you can look into its logs and find your application’s logs with the following command: $ kubectl logs -l app=petclinic … 2022-11-23 17:17:34.822 INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 3.972 seconds (JVM running for 4.248) You should be able to access the application by the cluster IP with $ curl http://192.168.49.2 Our test single-node single-pod deployment can be displayed on a scheme: Local Kubernetes cluster: single node, single pod Conclusion This tutorial taught us how to deploy Spring boot container images to minikube using a local registry. We also mastered the deployment configuration that we can adjust as required. In the following post, we will look closely at JVM tuning options and load testing imitating real-world loading in the Kubernetes environment. - [BellSoft’s Docker Hub images overview](https://bell-sw.com/blog/bellsoft-s-docker-hub-images-overview/): BellSoft provides a variety of ways to install its products. You can build your own image using any Linux distro and our binaries or package repositories. Alternatively, take advantage of our OCI container images by visiting BellSoft’s profile on Docker Hub . The Docker Hub images are grouped into repositories, each contains a number of tags pointing to an image. But given that we supply images for a wide range of system configurations and software versions, developers may be unsure where to look first. So we created an overview of our Docker Hub images that will help you to make an informed choice. Table of Contents Choose the operating system Alpaquita Linux base Alpaquita Linux and Liberica JDK Lite: a ready-to-go solution Alpaquita for other programming languages Liberica Lite with other operating systems How to pull a Docker image A walk through BellSoft’s Docker Hub repositories Choose the operating system We provide Liberica JDK container images for the most popular Linux distributions — Debian, Rocky Linux, Alpine Linux — and for our own lightweight operating system, Alpaquita Linux, optimized for cloud-native applications. Alpaquita is based on Alpine, but comes with several significant improvements: Userspace binaries protection Four malloc implementations in total Optimized standard C library implementation ‘musl perf’, both small and performant Base image size of 3.22MB, which is perfect for running small and performant containers Docker and QEMU support If you are looking for a way to increase the performance of your applications, save cloud resources, and receive regular security updates, head to Alpaquita repositories. Alpaquita Linux base The alpaquita-linux-base repository contains base Alpaquita images. The base image is only 3.22MB, perfect for small containers. It can be used ‘as is’ or customized by installing additional packages from Alpaquita repositories. There are two types of tags in this repo to take note of. The first one is the OS version: LTS or Stream, the latter being an upstream development branch. The Stream version has an alias tag with a version in the form of a date. Alternatively, use a stream tag with the latest OS release. The second tag points to the C library implementation: musl or glibc. Our musl perf is 100% compatible with stock musl, so if you have previously utilized a musl-based system such as Alpine, the migration won’t cause any issues. In case your OS is glibc-based and switching libc is off the table, consider Alpaquita Linux with glibc, which is more performant than popular glibc-based systems in some cases. For more information on Alpaquita Linux performance, refer to the comparative study overview. Image tag structure: [OS version]-[libc]-X, where X is the OS version number. For example, bellsoft/alpaquita-linux-base:stream-musl-221017 is a base Alpaquita Linux image, musl-based Stream version, released on October 17, 2022 Note that every repository has the ‘latest’ tag that points to the latest version of an image. Alpaquita Linux and Liberica JDK Lite: a ready-to-go solution We created Alpaquita Linux to help Java become a perfect language for cloud computing. Therefore, our containers including the only OS optimized for Java and a lightweight Java runtime are an end-to-end solution for enterprises working in the Cloud. The liberica-runtime-container repository contains Alpaquita images with Liberica JDK Lite and JRE Lite The liberica-native-image-kit-container repository contains Alpaquita images with Liberica Native Image Kit (NIK), a utility for converting JVM applications into native executables with almost instant startup time The repository with Liberica Runtime Containers includes additional tags with LTS Java (8, 11, 17) versions. In addition, you must choose a JDK type: jdk — Liberica JDK Lite version optimized for the Cloud jdk-all — a package with Liberica JDK that can be used to create a custom runtime with a help of jlink tool jre — Liberica JRE for running Java applications The Java version comes after the jdk type. For each Java version, there are JRE and JDK images for each Alpaquita libc type. Image tag structure: [JDK type]-X-[OS version]-[libc type]-Y, where X is the Java version, and Y is the OS version number. For instance, bellsoft/liberica-runtime-container:jdk-11-stream-glibc-20221018 is a container with Liberica JDK 11 and glibc-based Alpaquita Stream, released on October 18, 2022 As for the Liberica NIK Containers, the tags include the Liberica NIK version for each JDK type. Note that NIK supports only Java 11 and 17. For each Java version, there are also images for each Alpaquita libc type. Image tag structure: [JDK type]-X-nik-Y-[OS version]-[libc type], where X is the Java version, and Y is the NIK version. For example, bellsoft/liberica-native-image-kit-container:jdk-17-nik-22.2-stream-musl is a container with Liberica NIK version 22.2 for Java 17 and the latest version of musl-based Alpaquita Stream. In addition, we offer containers with support for Coordinated Restore at Checkpoint (CRaC), an OpenJDK API that helps to reduce startup and warmup times of Java applications from seconds to milliseconds. These images have a 'crac' tag. All images are ready to use, simply put your applications with dependencies into the selected container and that’s it! Alpaquita for other programming languages We also provide packages for other languages: Python and C/C++: The alpaquita-linux-python repository contains Alpaquita images with Python and basic Python utilities: pip, setuptools, wheel The alpaquita-linux-gcc repository contains Alpaquita images with the GCC compiler, tools, and libraries for development in C/C++ The tags include language version — Alpaquita currently supports Python 3.10 and GCC 11.2 — and libc type. For example, bellsoft/alpaquita-linux-python:3.10-stream-glibc is a glibc-based Alpaquita Stream, latest release, for Python 3.10 bellsoft/alpaquita-linux-gcc:11.2-stream-musl is a musl-based Alpaquita Stream, latest release, for GCC 11.2 Liberica JDK with other operating systems Non-Alpaquita repositories contain Liberica JDK or JRE for Alpine Linux, CentOS (deprecated), Rocky Linux, and Debian. First, choose a JDK or JRE repository. The available tags point at the Java version (right after the OS name) and supported architecture. Image tag structure: X-Y, where X is the Java version and Y is the architecture type. If the architecture type is not included into the name, then the build supports AMD64 by default. For instance, bellsoft/liberica-openjre-alpine-musl:18.0.2.1-1-aarch64 is an image with Liberica JRE version 18.0.2.1-1 for Alpine musl running on AArch64 bellsoft/liberica-openjdk-debian:17 is an image with Liberica JDK version 17 (the latest release) for Debian running on AMD64 All existing containers for operating systems can be substituted with a Liberica Runtime Container including Liberica JDK and Alpaquita Linux based on musl perf or glibc. This way, you will get a whole package for cloud-native Java apps: Microcontainers Liberica JDK Lite optimized for the Cloud environment with additional features improving memory consumption and performance Small and performant Alpaquita Linux Timely security patches and fixes both for JDK and Linux from one vendor Switch to the Liberica Runtime Container by changing one line in your Dockerfile. For instance, instead of FROM bellsoft/liberica-openjdk-alpine-musl:17 use FROM bellsoft/liberica-runtime-container:jdk-17-musl Note that our images include shell that supports exec, so for your RUN clause you can use not only the most common exec form but also a shell form as shown below: ENTRYPOINT exec java $JAVA_TOOL_OPTIONS -jar /app/app.jar How to pull a Docker image Download the image with docker pull: docker image pull docker bellsoft/: Start a container from a pulled image with docker run: docker run -it --rm bellsoft/: Alternatively, use a combined command: docker container run --rm -it bellsoft/: Note that if the : part is omitted, it is substituted with the :latest. A walk through BellSoft’s Docker Hub repositories We prepared an interactive scheme so that you could conveniently choose a suitable Liberica JDK image. A walk through BellSoft's Docker Hub images Browse BellSoft's Docker Hub repositories - [A guide to GraalVM — a next-generation JVM](https://bell-sw.com/blog/a-guide-to-graalvm-a-next-generation-jvm/): The global IT industry is well into the Cloud Age, and while companies eagerly adopt cloud migration strategies, the demand for a versatile, highly performant runtime increases. A perfect runtime environment enables developers to use the full potential of any programming language on one platform. At the same time, it increases the performance of an application and minimizes footprint. Wouldn’t the search for such runtime be like a chase for the Holy Grail? Well, there’s no need for chasing the Grail when you can build one — welcome to GraalVM! Table of Contents What is GraalVM Architecture Native image GraalVM advantages GraalVM is gaining popularity How to use GraalVM Liberica Native Image Kit — a powerful GraalVM-based solution Conclusion What is GraalVM GraalVM is a Java Virtual Machine (JVM) and Java Development Kit (JDK) written in Java. It aims to accelerate JVM-based applications and introduces other programming languages into the project, such as JavaScript, C/C++, Python, etc. Architecture GraalVM is based on the HotSpot VM, but unlike HotSpot C1/C2 compilers developed in C/C++, it uses its own Graal Just-in-time (JIT) compiler written mainly in Java, which enhances its modularity and maintainability. A layer of the JVM Compiler Interface (JVMCI, JEP 243) allows the JVM to use the Graal Compiler as a dynamic compiler. Graal JIT compiler includes numerous optimizations: more aggressive inlining, data flow analysis, and better escape analysis, to name a few. As a result, it provides superior performance compared to C1/C2 compilers. In addition to the Graal Compiler, GraalVM includes the Truffle language implementation framework (or Truffle), making it possible to work with non-JVM languages, such as Python, JavaScript, and others. Truffle is used for creating language interpreters and compiling them with Graal JIT Compiler so that the languages can run on the same JVM and exchange data. It implements a polyglot interoperability protocol — a set of messages implemented by programming languages — allowing smooth communication within multilingual apps. Truffle wasn’t initially developed to execute Java code, but now you can run Java applications using Java on Truffle mode for seamless Java interoperability with other languages. Finally, developers can utilize native languages such as C/C++ on GraalVM thanks to Sulong — an LLVM bitcode interpreter. GraalVM’s architecture can be schematically depicted as follows: Architecture of GraalVM Native image A powerful addition to GraalVM is the native image technology enabling Ahead-of-time (AOT) compilation instead of JIT. To understand why it matters, we must brush up on the mechanisms of compiling JVM-based apps. Just-in-time (or dynamic) compilation is the process of converting the JVM bytecode into machine code after the program starts, i.e., at runtime. The JIT compiler analyzes the code, searches for hotspots (the most frequently used code parts), decides which OS the app runs on, etc. After that, the compiler performs multiple optimizations to produce highly performant code. JIT compilation is perfect for long-running, high-throughput applications. But it has certain drawbacks, namely a larger footprint and increased startup time. With AOT (or static) compilation, the bytecode is converted into machine code before the program execution. The AOT compiler eliminates dead code and creates an executable specific to the OS and architecture, which accelerates the startup and optimizes memory consumption. The resulting native image starts up almost instantly and reaches peak performance without warm-up. In addition, native images don’t require JVM to run, providing a smaller footprint. However, AOT can’t handle the dynamic features of Java and can’t perform some optimizations such as adaptive optimization or function inlining. So it is suitable for applications where fast startup is a primary goal. GraalVM advantages Why do we need another JVM? Isn’t the good old HotSpot good enough? True, properly tuned HotSpot can provide excellent performance for various use cases. Originally, the primary objective of the GraalVM project was a more performant, easy-to-maintain, and enhanced JIT compiler. But with the outspread of microservices and cloud computing, GraalVM has revealed other significant advantages: Native images with next-to-instant startup eliminate the issue of cold starts in the cloud, which is especially important for Amazon AWS Lambda (and similar services from other cloud providers). Amazon Lambda is a pay-per-use function running the code in response to events only. The service is shut down when nobody uses the app and has to be started again when triggered. Traditional JIT compilation needs time for optimizations so that a cold start can stretch out for several seconds, irritating users and leading to lost profit. Graal AOT compilation gets the benefit of cost-efficient Lambdas and, at the same time, enhances customer experience GraalVM makes extensive polyglot projects a reality. Companies create microservices using any programming language and have no trouble establishing communication between them. Furthermore, it is possible to integrate packages and libraries from different languages fitting perfectly into a GraalVM-based app. For instance, a Java application can use R packages for enhanced data processing or Python for machine learning functions. Applications boosted with Graal JIT capabilities are fast, provide higher throughput, and require fewer resources, which translates into lower cloud bills. Not only cloud-native applications benefit from GraalVM. JavaFX programs compiled into native images don’t require Java installed on the machine to run. The size of the executable can be seven times less than a traditional jar with JRE, and it also starts much faster. The cherry on top of the GraalVM cake is that it is based on OpenJDK and Oracle JDK, so the migration will not be associated with code changes. GraalVM is gaining popularity GraalVM is young, powerful, and full of energy and ambition. No wonder it has gained so much traction in recent years. Major Java frameworks have adopted the technology: Quarkus Micronaut Spring Boot Helidon Note that Spring Native has been integrated in Spring Boot 3, which means it recieved a baked-in support for GraalVM Native Image starting with version 3.0.0-M5. In addition, major industry players — Twitter, Alibaba, Facebook, Oracle Cloud Infrastructure (OCI) Monitoring service, and many others — integrated GraalVM into their workloads and reported better performance and decreased memory and CPU consumption. GraalVM is an open-source project, and given that it is written in Java and has a cleaner code base compared to C-based HotSpot with 20 years of history, it is much easier to optimize and enhance. Indeed, innovations are fast to arrive. Every quarterly GraalVM release brings something new to the community. For instance, the latest 22.3.0 version contains the following Java updates: Added support for JDK 19 and JEP 425: Virtual threads enabling the development of high-throughput concurrent Java applications Improved JIT compilation isolation to reduce GC pause times Improved the debugging process for memory leaks identification Reduced the size of the Native Image installable by shipping the LLVM backend as a separate bundle Other enhancements Any developer can technically contribute to GraalVM. OpenJDK has already proven the viability of the open-source philosophy and enjoys constant fixes and enhancements from an extensive developer community. But when large companies are interested in the project, it indicates great prospects for the solution. Industry leaders such as Oracle, Red Hat, BellSoft, Alibaba, etc., engage in driving GraalVM forward by bringing new features and innovations to the project. For instance, BellSoft engineers have recently prepared a work-in-progress GitHub pull request to implement Parallel GC to GraalVM Native Image, which will be a significant step towards concurrent garbage collection within the native image. How to use GraalVM There are two editions of GraalVM: GraalVM Community based on OpenJDK and GraalVM Enterprise based on Oracle JDK. GraalVM CE is open source and, therefore, free to use but offers no commercial support. GraalVM EE comes with 24x7 support and additional features that haven’t made it to the community edition yet, such as support for Apple Silicon or software bill of Materials (SBOM). In addition, GraalVM EE includes the G1 garbage collector absent in CE. Getting started with GraalVM is simple. Choose the OS on the official site and follow the installation instructions to download the core distribution with JVM and Graal Compiler. It will enable you to run Java apps. Runtimes for other languages can be installed with the gu tool. It is also possible to use the native-image function separately from the main GraalVM distribution. Although the AOT compilation, added as an experimental feature to Java 9, was removed from Java 16, it is provided as part of standalone GraalVM-based solutions. Liberica Native Image Kit is one of them. Liberica Native Image Kit — a powerful GraalVM-based solution BellSoft is actively involved in GraalVM development, being part of the GraalVM Project Advisory Board and contributing fixes and improvements. So we used the high-level expertise of our engineers to create Liberica Native Image Kit (NIK), a utility based on GraalVM Community Edition for native image generation. The Spring team trusts the quality and reliability of Liberica and utilizes it as the default native-image compiler in Spring Boot. So what do you get with Liberica NIK? The tool is always based on the latest version of Liberica JDK 11 & 17 and GraalVM, with security patches, bug fixes, and improvements for you to enjoy the max. security and fresh features at all times Wide range of supported platforms — Linux, macOS, Windows — including musl and glibc support, so you can use Liberica NIK with a musl-based OS (for instance, Alpaquita Linux) and drastically reduce the size of your containers Three flavors are available for enhanced flexibility: Core, Standard, and Full. The Core package contains only Liberica VM and native image for Java development. The Standard package supports language installables for polyglot projects. The Full package includes LibericaFX based on OpenJFX to develop rich web and desktop applications. Choose the package that best suits your needs and save memory resources. Liberica NIK is an open-source utility and can be used for free. But we also provide commercial enterprise-grade support with 24/7 access to our experts, emergency security patches, and prompt fixes based on SLA The support also covers Liberica JDK, a progressive Java runtime with the widest range of supported system configurations, or Alpaquita Cloud Native Platform, a technology stack for cloud-native Java applications. ACNP is based on Alpaquita Linux, a minimalistic, performant, and secure containerized Linux, highly efficient with Java and Native Image workloads.Learn more about Alpaquita Linux But less talk, more action! You can try out Liberica NIK right now and see how it fits perfectly into the existing infrastructure and helps launch the apps in a fraction of a second! Download Liberica NIK Furthermore, take a look at our guides with step-by-step instructions on integrating Liberica NIK with the most popular Java frameworks or turning JavaFX apps into native images: Generate a native executable with Spring Native Create a microservice with Micronaut framework Build a native image with Quarkus framework Learn to convert JavaFX applications into native images Conclusion GraalVM paves the road to the world of cloud-native polyglot programming. It uses Java to build a sustainable, maintainable, and highly performant platform that can take in any programming language to use its strengths for the sake of your project. Embrace the innovation now! Install GraalVM or experiment with native images. See how Liberica NIK transforms your services into lightweight, high-speed, performant executables and puts Java on par with Go or Rust in terms of Cloud costs. At the same time, it enables you to use all the best practices, tools, and developers expertise. We understand that introduction of Native Image requires deep code refactoring, so large companies hesitate with migration. Fortunately, there exists a simpler way to save Cloud resources — Alpaquita Cloud Native Platform. Click on the button below to learn more about the solution that will help you immediately cut the expenses. Discover Alpaquita Cloud Native Platform - [Why Java is the right choice for agile software development](https://bell-sw.com/blog/why-java-is-the-right-choice-for-agile-software-development/): Agility — the ability to respond to changes rapidly and adapt to shifting market trends — is the only way for an enterprise to stay successful in a turbulent world. As far as the IT industry is concerned, agile software development means providing value to users in small increments rather than large releases while continuously evaluating feedback and requirements. It allows the team to introduce changes and generate value in market dynamism quickly. Agile is a mindset described in the Manifesto for Agile Software Development. This article will not delve deep into how to build an agile enterprise. We will make a case for Java as a perfect language for Agile and give you a solution for laying out the pipeline for delivering a high-quality product to customers in an ever-changing environment. Table of Contents Java changes rapidly Java is versatile Java exists in an extensive ecosystem of frameworks and tools Java facilitates test-driven development Java facilitates incremental releases Alpaquita Cloud Native Platform: an end-to-end solution for agile enterprises Java changes rapidly A perfect programming language for software development embraces Agile methodologies itself. With six releases per year — two functional ones and four CPUs — improvements are introduced incrementally, and new features are added constantly in response to developers’ demand and general IT trends. Features are first introduced in Experimental, Preview, or Incubator mode before becoming permanent so developers can try them out and give feedback. On the contrary, some features are marked as deprecated and later removed because APIs evolve, better methods arrive, and there’s no need to keep an obsolete feature around. In addition, there are projects developed parallel to the main OpenJDK project. They promise global changes to Java code; therefore, they need extensive testing before merging them with OpenJDK. The latest example is the introduction of virtual threads (part of Project Loom) to JDK 19 that take Java concurrency to a new level. By embracing the newest Java features, developers can enhance the product they work on in the most convenient and modern way. Java is versatile Building upon the previous statement, the enhancements introduced to Java make it the ultimate language for any system or environment. A company can safely introduce new processes and platforms or migrate to the Cloud — Java will support them all: Do you want to migrate an application to the Cloud or create a cloud-native solution from scratch? With Java, you can build small and performant containers, Docker containers with the Spring framework and Maven plugin, and use Cloud Native Buildpacks to accelerate the deployment Do you want to embrace innovation and experiment with RISC-V? Java performs perfectly well on the embedded market and has a RISC-V port implemented since JDK 19 Do you want to accelerate development and testing? Build a Java-based container compatible with all system configurations. It can be used for development, testing, and deployment, thus accelerating the CI/CD pipeline and saving time for your teams Do you have a mobile app and want to build a desktop version for enhanced user experience? Use JavaFX, an open-source platform for developing rich client applications Moreover, you may notice deterioration in the performance of your product with load increase or due to some other external factors. With Java, you will be able to react promptly to those changes thanks to numerous performance tuning techniques and solutions for performance enhancement. Java exists in an extensive ecosystem of frameworks and tools For 27 years, Java has accumulated a rich ecosystem of tools, libraries, and frameworks that enable companies to meet the requirements for modern software development. The most popular Java frameworks — Spring, MicroProfile, Quarkus, etc. — help developers create any application. In addition, the frameworks above offer numerous features that simplify development and testing. For instance, they support Constructor-based Dependency Injection, which helps to achieve loose coupling and avoid testing errors, and increases thread safety in multi-threaded environments. So Java enables the developers to use advanced methodology and patterns, such as CDI, AOP, etc. Java functions in powerful IDEs: Eclipse IDE, IntelliJ IDEA, and NetBeans. They equip the developers with a rich toolset and code assistance that facilitate and accelerate development. In addition, they promote team cooperation with utilities for code review, project management, CI/CD tools, etc. Maven and Gradle — open-source build tools for Java applications — automate code compilation, testing, and packaging, thus making the build process convenient and reliable and allowing for shipping high-quality software faster GraalVM — a new Java Virtual Machine based on HotSpot but with Graal as a compiler — makes polyglot programming a reality. It supports the seamless integration of multiple programming languages (JVM and non-JVM-based) into one project and their interoperability. Companies can add new features to their software products using the strengths of Java, Python, C++, etc., enhance cooperation between teams working with different languages, and all of that in the Cloud! The fact that Java supports a large variety of technologies gives room to creative thinking and experiments, which are an integral part of Agile methodologies. Java facilitates test-driven development Test-driven development (TDD) is one of the best Agile software development practices. It ensures that bugs are caught during development rather than by end-users and increases code quality and developer productivity. Java provides excellent tools to integrate TDD at the company, for instance: Various testing frameworks such as JUnit and Mockito. JUnit provides tooling for unit tests when individual modules of software are tested. Mockito is a mocking framework that can create mock classes and interfaces to test specific behavior. JMeter, a utility for performance testing. It can be used for load simulation to evaluate software performance under different loads and thus avoid unexpected behavior later on Testcontainers, a Java library that provides lightweight instances of databases or any technology that can run in a Docker container for data access, application integration, UI testing, and so on Java facilitates incremental releases Java promotes incremental software releases through sensible dependency management with Maven or Gradle, Maven Central, multiple testing frameworks described above, and simple containerization. Java-based containers can be as small as 42,72 MB reducing the push/pull times from 30 s to a couple of seconds, so your developers can test the code or send it to production much faster. In addition, there are different approaches to packaging a Java application: thick, thin, or skinny jars. The last two are optimal solutions for dev and test environments. With thin or skinny jars, your container doesn’t include app dependencies, which are stored in a local repository, so the package size is minimized, and deployment times are decreased Layered container images. By separating app components into layers, you can put the most frequently used ones on top and leave others on the bottom. So when you have to update the image, only the top layers are affected Key solutions for CI/CD, such as Jenkins and TeamCity, are based on Java and so are the perfect match for companies utilizing Java. Alpaquita Cloud Native Platform: an end-to-end solution for agile enterprises What if we took Java, reinforced it with a lightweight Linux distribution, and put it into a small customizable container? BellSoft engineers created a versatile solution for Cloud and server development — Alpaquita Cloud Native Platform (ACNP), which includes Liberica JDK, a progressive Java runtime with the broadest range of supported systems Alpaquita Linux, a small Linux distribution (base image only 3.22 MB) tailor-made for Java. Alpaquita comes with two libc implementations (optimized musl and glibc) and four mallocs for any workload Liberica Native Image Kit, a utility for generating native images with startup time reduced to 1/10s LTS support from a single vendor for both Java and Linux ACNP is a perfect match for Agile software development: it enables you to unify and standardize the corporate tech stack, thus eliminating compatibility issues and accelerating CI/CD. ACNP is suitable for multi-cloud, hybrid cloud, or server applications, so you won't need additional instruments even if you decide to shift environments or expand your business. Alpaquita Linux is fully customizable: remove or add packages from Alpaquita repositories for your purposes. Finally, ACNP provides the smallest containers, always secure and performant, thanks to timely updates. Work with a single vendor who will take care of your runtime and OS and concentrate on generating value for your customers! Learn more about ACNP - [How to reduce the size of Docker container images](https://bell-sw.com/blog/how-to-reduce-the-size-of-docker-container-images/): Docker images are often filled with software components your application doesn’t need to run. As such, they slow down the development process and take too much space in the cloud that you have to pay for. How do we reduce the Docker image size? Find out in this article! Table of Contents Understanding Docker Images Basic Structure Common Issues Best Practices for Reducing Docker Image Size Choosing a Base Image Minimizing Layers Optimizing Dockerfile Instructions Using Minimal Packages Removing Unnecessary Files Analyzing and Monitoring Image Size Optimizing Application Dependencies Using Slimming Tools Case Studies and Examples How to create a custom JDK container image Conclusion Understanding Docker Images Basic Structure Before adjusting the container size, we must understand its structure. The software in containers forms a stack. The top layers with an application, its dependencies, and OS packages are the most frequently changed ones. A parent image and a base image compose the bottom layers. We rarely change base layers, so there are solutions for patching separate container layers to accelerate update times. As per Docker documentation, a parent image is the one that your image is based on. It refers to the contents of the FROM directive in the Dockerfile. If the parent image is SCRATCH, then it is considered a base image. Most Docker images start from a parent image rather than a base image. However, these two terms are often used interchangeably. The structure of a typical Docker image can be depicted as follows. Container layers Common Issues So why are Docker images prone to bloating? The most common root causes include Unnecessary base OS packages, JDK instead of JRE for Java applications, Unnecessary image layers. In the subsequent sections, we will look into methods of adjusting the layers to keep the image lightweight yet performant. Note that there are two types of images — for developing and deploying applications. We will focus on containers for deployment. Best Practices for Reducing Docker Image Size Choosing a Base Image There are two ways to reduce the base image size: use distroless images or lightweight Linux distributions tailored to containers. Distroless images are designed to be as small as possible. They contain only the packages necessary for the application to run and no usual Linux components such as shell or a package manager. The smallest distroless image takes up only 2MB, but it is suitable for specific statically-linked applications. Distroless images for applications that require a lbc to run JRE, or have dynamic features,are a lot bigger: the size ranges up to 200 MB! To learn more about distroless and whether these images are suitable for your project, refer to the article Are distroless images small and secure? Another option is to use a small Linux distribution for a base image. But the variation between Linux image sizes is quite significant. Alpaquita (musl) Alpaquita (glibc) Alpine (musl) RHEL (Distroless UBI 8 Micro) Ubuntu Jammy Debian Slim Container image size (compressed) 3.57MB 11.55MB 3.46MB 11.3MB 28.17MB 30.72MB Size is not the only factor — albeit quite a substantial one judging by the cost of Cloud resources — when selecting a Linux distribution. Other important characteristics include available and affordable commercial support, LTS releases, security features, C library implementation, etc. A detailed comparison of popular Linux distributions for cloud and server can be found in our previous article. But as you can see, you can significantly reduce the Docker image by migrating to another OS. When it comes to minimizing resource consumption, Alpine Linux has been a distribution of choice for a long time. Alpine is an open-source community-driven project. It is based on musl libc in contrast to other popular distributions utilizing glibc. Coupled with the fact that it includes only the essential OS packages, it is a great minimalistic and secure distro for containers. But Alpine has several drawbacks: There’s no enterprise support; Stock musl implemented in Alpine may have worse performance than glibc in certain cases; There are no builds with glibc libc, which may make migration from glibc-based distros challenging. If these factors are critical to you, consider Alpaquita Linux. It is a lightweight open-source distribution inspired by Alpine, but with several enhancements such as Enterprise support with LTS releases; Two versions: with optimized musl that demonstrates equal or superior performance to that of glibc, and with glibc. Alpaquita can be used with various programming languages. For running Java applications, there’s a Liberica Runtime Container that takes up only 52MB. For most users, such image size reduction will already be enough to drastically lower сloud resources consumption even without further adjustments. Minimizing Layers Each layer of a Docker image represents an instruction in the Dockerfile. Commands in the Dockerfile that modify the filesystem create a new layer. So each RUN, COPY, ADD command adds a new layer to the image, thus increasing its size. So instead of running FROM ubuntu:latest RUN echo somedata RUN mv somefile We can merge the requests into one command: FROM ubuntu:latest RUN echo somedata && mv somefile Yet, an even better approach is to use Docker multi-stage builds. With this technique, you write one Dockerfile that contains several FROM statements, with each FROM statement using its own base image and starting a new build phase. You can copy only the artifacts you need at a new stage. This way, the final image won’t contain layers from the previous build phases. Let’s see how we can implement multi-stage builds using Spring Petclinic as an example app and Liberica Runtime Container as a base image: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder WORKDIR /home/app ADD spring-petclinic-main /home/app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw clean package FROM bellsoft/liberica-runtime-container:jre-21-musl WORKDIR /home/app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/petclinic.jar"] COPY --from=builder /home/app/spring-petclinic-main/target/*.jar petclinic.jar Another way to minimize the number of layers is to use the docker-squash tool, which squashes the last N number of layers into one. This will help you keep layers with temporary or deleted files out of the image. So if you created a lot of large files (for instance, added Spring resources) and then deleted them in a new layer, you can squash the image and reduce its size several times. Optimizing Dockerfile Instructions The fact that each instruction in the Dockerfile creates a new layer gives rise to another issue not strictly related to the size of the image, but to the image build time. As layers are placed on top of each other, whenever you update a layer, it must be rebuilt together with all consecutive layers. So, it is better to place less frequently updated layers on the bottom and shift more frequently updated ones towards the top of the Dockerfile. For instance, the following Dockerfile FROM bellsoft/alpaquita-linux-base:stream-musl COPY . . RUN apk add --no-cache liberica17-lite-jdk-all Will trigger the reinstallation of dependencies if any of the project files are changed. Instead, we can do that: FROM bellsoft/alpaquita-linux-base:stream-musl RUN apk add --no-cache liberica17-lite-jdk-all COPY . . This way, Docker will cache the installed dependencies and use the data from cache next time you have to change the following layers. In addition, it is advisable to remove any temporary files in the same command you’ve been working with them. FROM bellsoft/alpaquita-linux-base:stream-musl RUN apk add --no-cache liberica17-lite-jdk-all \ # use jlink to cut out a custom jre && apk del liberica17-lite-jdk-all Using Minimal Packages One of the reasons for Linux's immense popularity is customization. You can eliminate unnecessary packages and add modules required for your project, thus keeping the distribution clean and compact. In most cases, a base Linux image already contains numerous modules, which can be later removed manually. However, starting with a minimal set of packages is more manageable, like in the case of Alpine and Alpaquita. The micro base image can be used as is for simple tasks or Lambdas. But you can pull the rest of the essential packages from Linux repos. You should also reduce the number of dependencies by removing unnecessary ones or monitoring that unrequired dependencies are not added. Package managers can be helpful if you want to control the installation of dependencies. Direct dependencies are essential for some tasks, and indirect (optional) ones might be beneficial. The most popular Linux package managers (apt, yum, etc.) install all dependencies by default. You can regulate this behavior. For instance, the command with Ubuntu/Debian $ apt-get install -y --no-install-recommends package name Ensures that optional recommended packages are not installed. As for Alpaquita and Alpine, there’s no need for a similar command because these distros install only direct dependencies. Removing Unnecessary Files The image build process implies copying files from the host into the build context. But the project often contains large and unnecessary files not required for the application to run. The .dockerignore file enables the developers to specify files and directories that shouldn't be copied into the image. This helps to accelerate the build process and reduce the image size. It also increases security by eliminating the risk of putting sensitive data (commit history, credentials, etc.) into the image. In addition, package managers have a cache where they store installed packages and other files. But we don’t need it in the Docker image. So you should clean the cache before building an image. For Debian/Ubuntu, run $ apt-get clean With Alpaquita and Alpine, you can utilize the command $ apk add --no-cache to avoid storing the package in the cache in the first place. Find out more tips on working with APK in a dedicated guide. You can also exclude man pages and documentation from your Docker image if you don’t need them. Analyzing and Monitoring Image Size When optimizing the Docker image size, it is important to understand what is going on in your image: which layers it contains, which layers take the most space, and so on. The usual docker images command gives only the general info on the docker image size, but there are other tools you can use to peek into your image structure. First of all, you can use docker image history [IMAGE] that displays the information about image layers and their size, for instance: $ docker image history IMAGE CREATED CREATED BY SIZE COMMENT 6988370c72b1 About a minute ago CMD ["java" "-jar" "petclinic.jar"] 0B buildkit.dockerfile.v0 About a minute ago COPY /home/app/spring-petclinic-main/target/… 61.2MB buildkit.dockerfile.v0 7 minutes ago WORKDIR /home/app 0B buildkit.dockerfile.v0 12 days ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B 12 days ago |8 LIBERICA_BUILD=9 LIBERICA_GENERATE_CDS=fa… 129MB 12 days ago /bin/sh -c #(nop) ARG LIBERICA_GENERATE_CDS… 0B 12 days ago /bin/sh -c #(nop) ARG LIBSUFFIX=-musl 0B 12 days ago /bin/sh -c #(nop) ARG LIBERICA_USE_LITE=1 0B 12 days ago /bin/sh -c #(nop) ARG LIBERICA_RELEASE_TAG= 0B 12 days ago /bin/sh -c #(nop) ARG LIBERICA_ROOT=/usr/li… 0B 12 days ago /bin/sh -c #(nop) ARG LIBERICA_VARIANT=jre 0B 12 days ago /bin/sh -c #(nop) ARG LIBERICA_BUILD=9 0B 12 days ago /bin/sh -c #(nop) ARG LIBERICA_VERSION=21.0… 0B 2 weeks ago /bin/sh -c #(nop) ENV LANG=en_US.UTF-8 LANG… 0B 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B 2 weeks ago /bin/sh -c #(nop) ADD file:a71f7e9bc66668361… 8.83MB Another useful tool for analyzing image contents is dive. The tool displays information about the image layers, changes to the file tree, and estimates image efficiency. Using it is as simple as running $ dive And the screenshot below shows an example output. Image evaluation by dive Optimizing Application Dependencies Packing the application with all its dependencies into a Docker image may significantly contribute to its size. Let’s see how we can optimize dependencies using Java applications as an example. Thin JARs A traditional way of packing a Java application into an executable is building a fat JAR. A fat JAR or uber-JAR contains application class files, application, and all its dependencies, resulting in a self-contained executable that needs only a JRE to run. But we can trim down the size of our executable by creating a thin JAR that includes the application code without the dependencies. The dependencies are stored in the local repository, so there's no need to push the application with all dependencies across the dev, test, and prod environments, thus increasing process efficiency. A thin JAR forms a separate container layer leaving the same overall size and a tiny update portion. It is also possible to utilize class files without the JAR packaging, which accelerates startup and reduces compressed image size due to the absence of double compression. Using thin JARs also lets us separate the layers of a container image and put the ones that get frequent updates on top. This method saves the developers a lot of time when they need to introduce changes to the application because only the top image layers get affected. Another possible solution is to use Application Class Data Sharing (AppCDS), a JVM feature that loads a set of pre-initialized system classes and application classes into an archive that can be shared by multiple JVM processes. The main goal of AppCDS is to reduce Java application startup, but it can also help you reduce footprint depending on the number of application instances that share the archive. Great news is that Spring Boot 3.3 comes with support for CDS, so creating an archive is extremely convenient. CDS with Spring Boot apps yields about 40% faster startup, and if you couple it with Spring AOT the startup is more than 50%! Find out how to use CDS with Spring Boot in this tutorial. JRE images We need JDK (Java Development Kit) to develop Java applications. It includes Java Virtual Machine (JVM), Java Runtime Environment (JRE), and development tools, such as a debugger, compiler, etc. To run Java apps, we need only JRE, which includes JVM and specific classes for program execution. Developers sometimes put JDK into the containers aimed for app deployment, increasing their size unnecessarily, while JRE images are more suitable for use in production. To compare, Liberica Runtime Container with Alpaquita Linux and JDK, including tools such as jlink, takes up about 180 MB JDK Lite, optimized for Cloud, takes up about 85 MB JRE is only about 50 MB In the Minimizing layers section we showed how to use a base image with JRE. Using Slimming Tools The DockerSlim tool (docker-slim) automatically optimizes the size of a Docker image. It creates a temporary container and decides which files an application requires using various analysis techniques. The resulting single-layer image with only necessary files can be 30 times smaller than the original one, thus reducing memory consumption and enhancing security due to a minimal attack surface. However, the tool should be used with caution. docker-slim may accidentally throw away the files the application needs due to lazy loading. It may lead to production errors or even an unusable container. To avoid such situations, use the --http-probe and --include-path flags to detect all dynamically loaded functions and preserve required files. It may be more complicated with Java, because a typical Java API contains multiple dependencies, and some of them can be unobvious and nonstatic. Case Studies and Examples How to create a custom JDK container image In this section, we will build a custom JDK image using jlink, Liberica JDK Lite and Alpaquita Linux. First, choose a C library implementation. Alpaquita Linux offers two libraries with three versions: musl-perf optimized for performance musl-default (upstream build) glibc If you choose musl-perf, the Dockerfile will start with FROM bellsoft/alpaquita-linux-base:stream-musl Note that Docker images with musl-based Alpaquita contain musl-perf by default. If you want to switch to the upstream version, add the following command to the RUN instruction: RUN apk add musl-default Next, choose a Java version. Only new LTS versions, JDK 11, 17, and 21 currently support jlink. If you are using Java 8 for enterprise development, this is a good incentive to migrate to a newer version. We will build a custom image based on Java 17. For this purpose, install the package liberica17-lite-jdk-all, which contains everything you may need to build the image. RUN apk add --no-cache liberica17-lite-jdk-all Configure jlink execution parameters. The most important ones are: --add-modules — this option allows us to specify only those modules which we really need. We choose the java.base module which contains the essential implementation of reading classes and resources --vm — we will use server as the most suitable option for user needs --no-header-files — we don't need headers as we aren’t going to compile JNI code --no-man-pages — we don't need documentation --compress <0/1/2> — 0 means No compression, 1 is Constant string sharing, 2 is ZIP. The compression level affects the disk size of the runtime image. The highest compression level results in a smaller image, but with a potential penalty to startup. Another problem with compression is that the resulting container image will also be compressed, but with Zip, the result will be less efficient as it could be in case of no compression. In other words, if you want to save the network bandwidth, use 0, other options may require additional experiments --strip-debug — we don't need debug information, and this option reduces size of the runtime image by approx. 20% --module-path — a path to Java Modules (jmods), usually it's $JAVA_HOME/jmods --output — a path to the location where the resulting image will be created Let’s summarize it all for our Dockerfile: RUN apk add --no-cache liberica17-lite-jdk-all \ && jlink \ --compress=2 \ --no-header-files \ --no-man-pages \ --strip-debug \ --module-path $JAVA_HOME/jmods \ --vm=server \ --output /opt/customjdk Now, let’s remove our supplementary package liberica17-lite-jdk-all because we don't need it anymore: && apk del --no-cache liberica17-lite-jdk-all The --no-cache option is not really required, but without it, apk will issue a warning that there are no index files. Next, add necessary environment variables: ENV JAVA_HOME="/opt/customjdk" ENV PATH="$JAVA_HOME/bin:$PATH" Add the default execution command, i.e., when it's run without any parameters. It will show the Java version: CMD ["java", "-version"] Below is the resulting Dockerfile. FROM bellsoft/alpaquita-linux-base:stream-musl RUN apk add --no-cache \ liberica17-lite-jdk-all \ && jlink \ --add-modules java.base \ --compress=2 \ --no-header-files \ --no-man-pages --strip-debug \ --module-path $JAVA_HOME/jmods \ --vm=server \ --output /opt/customjdk \ && apk del liberica17-lite-jdk-all ENV JAVA_HOME="/opt/customjdk" ENV PATH="$JAVA_HOME/bin:$PATH" CMD ["java", "-version"] We can now build the image: $ docker build . -t customjdk Finally, let’s run our image: $ docker run --rm -it customjdk openjdk version "17.0.12" 2024-07-16 LTS OpenJDK Runtime Environment (build 17.0.12+10-LTS) OpenJDK 64-Bit Server VM (build 17.0.12+10-LTS, mixed mode, sharing) We can check the size of the image with the following command: $ docker inspect --format '{{.Size}}' customjdk 40300400 The Docker image size is only about 40.3MiB. And its compressed size will be around 20.4MiB. You can play with the --compress option described above to achieve the desired result. Of note is that the same image with Minimal VM (--vm minimal) has an uncompressed size of 24.4 MiB and compressed size of 14.93 MiB. Conclusion As you can see, there are numerous ways of keeping Docker container images clean and lightweight. The key takeaway — keep unnecessary files out of the image and use the smallest base image possible. - [Six articles to read over the long weekend](https://bell-sw.com/blog/six-articles-to-read-over-the-long-weekend/): Here are our best articles you can enjoy during the holidays if you miss your work as much as we do. Or maybe just read them after the long weekend, which is totally fine. Enjoy! Discover the common myths surrounding the OpenJDK builds and reasons to consider switching to Liberica JDK. Explore the differences between the popular Linux builds for servers and cloud and find the one that works for you. Learn to turn JavaFX applications into native images. Find out how to use perf to monitor Java performance. Get acquainted with Java Flight Recorder (JFR), a useful tool for diagnostics. Check out the common mistakes developers make when they containerize their apps and learn how to avoid them. Once again, we wish you a happy holiday and a productive 2023! - [BellSoft’s annual results 2022](https://bell-sw.com/blog/bellsoft-s-annual-results-2022/): 2022 was a turbulent year, but it’s over, so people all over the world are taking a break and contemplating what they managed to accomplish. For us at BellSoft, this was a year of growth, testing new waters, and, as always, trying to do the things we do better. Please allow us to share some of the victories we were able to win: Liberica JDK keeps improving Liberica JDK maintains its status as one of the leading OpenJDK distributions of 2022 thanks to its many features, great support, and availability on the largest number of platforms. Its rising popularity led to the notable increase of downloads by 70%. As a result of the trust our partners put into Liberica JDK, BellSoft’s revenue grew by 92%. Liberica NIK received critical updates For Liberica Native Image Kit, there was a next major version released (Liberica NIK 22), and all the available versions were provided with builds based on JDK 11 and JDK 17. The popularity of Liberica NIK led to a 1,800% increase in the number of downloads. BellSoft’s products are recommended for Spring Framework Our collaboration with VMware is going strong. Liberica JDK became an OpenJDK distribution recommended by the Spring Framework team. Now that Spring Boot 3 received a baked-in support for GraalVM Native Image, superseding the Spring Native project, Liberica NIK continues to be a recommended Native Build Tool. New line of products is released 2022 became a year when we presented a new operating system and the whole Platform based on it for creating performant lightweight containers! Alpaquita Linux is the tiny performant OS built for containers and optimized for Java applications. By itself, it is a modern Linux full of features and with both glibc and musl builds available. Together with our other software, it becomes something more — Alpaquita Cloud Native Platform! Alpaquita Cloud Native Platform is a set of fundamental technologies for containerized Java services execution. With it, we were able to make our tiny JDK container images even smaller, more secure, and more performant. And thus, with Alpaquita images, we managed to make our set of containers even more complete. Contribution In 2022, we continued to participate in the work of the OpenJDK community. OpenJDK We stayed among the top companies contributing to OpenJDK mainline and LTS update releases. Our engineers made notable enhancements for the OpenJDK project, including multiple improvements to code density and cryptography. BellSoft maintained its status among the leading OpenJDK contributors with the release of JDK 18 and JDK 19. Our CTO, Aleksei Voitylov, was re-elected as a member of the JCP Executive. GraalVM This year, we achieved something new by doing more work in the GraalVM community. We started implementing the parallel garbage collector (in progress). This is important because currently there is an issue of resources not being used efficiently by runtime inside the native images with their single thread stop-the-world mechanism utilized. Garbage collector inside these native images is very useful, but it takes time to finish, and during that time the application is paused. Our tests show that with the parallel garbage collector we can raise throughput and radically decrease the GC pause, thus making the latency much lower. JDK 6 & 7 2022 was the year when Oracle ended the extended support for JDK 7, and JDK 6 exists without Oracle’s support since 2018. And yet both of these Java versions are utilized by many enterprises all over the world as they have multiple reasons for steering clear of upgrading. BellSoft is the company that provides support for JDK 6 and 7 and will keep doing so at least up to 2026. With quarterly security updates, cryptography maintenance, updates to IANA timezone data and root certificates, and fixes for functional regression and other issues, we make Liberica 6 & Liberica 7 secure and reliable. Our runtime is compatible with multiple virtual environments and 32 and 64-bit operating systems. Conclusion During the past year, we worked to fulfill our vision of making Java the number one technology for modern enterprises, and we will continue doing so with passion! We promised to continue providing high-quality support and creating innovative tools, and this year proved we can deliver this and much more! Of course, all of this was made possible by you, our customers, partners, OpenJDK community, Java developers, students, and enthusiasts. As always, we are ready to help and support you, so, please, contact us with any questions or concerns. Let’s keep making Java better together as a community! We are always happy to talk! - [Liberica JDK 8u362, 11.0.18, 17.0.6, and 19.0.2 released](https://bell-sw.com/blog/liberica-8u362-11-0-18-17-0-6-and-19-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 8u361, 11.0.17.0.1, and 17.0.5.0.1. CPU releases include patches for Common Vulnerabilities and Exposures (CVE). In addition, we release PSU versions 8u362, 11.0.18, 17.0.6, and 19.0.2 with non-critical fixes and general improvements. The release contains 778 fixes and backports overall. BellSoft participated in eliminating 24 issues in all releases. Table of Contents How to keep your runtime secure The summary of fixes List of security issues fixed Summary of fixes in Liberica JDK Supported platforms Enjoy the most stable runtime! How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. The summary of fixes 3 security issues (CVEs) fixed 35 total security fixes in CPU release: in Liberica 8u361: 11 security fixes + 0 in FX in Liberica 11.0.17.0.1: 12 security fixes + 0 in FX in Liberica 17.0.5.0.1: 12 security fixes + 0 in FX In addition, PSU releases include a total of 743 bugs and backports fixed: in Liberica 8u362: 18 security fixes + 61 additional fixes (+ 9 in FX) in Liberica 11.0.18: 19 security fixes + 215 additional fixes (+11 in FX) in Liberica 17.0.6: 19 security fixes + 272 additional fixes (+18 in FX) in Liberica 19.0.2: 18 security fixes + 83 additional fixes List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-21835 5.3 security-libs javax.net.ssl network low none none unchanged none none low CVE-2023-21830 5.3 other-libs - network low none none unchanged none low none CVE-2023-21843 3.7 client-libs javax.sound network high none none unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 19 CVE-2023-21835 - • • • CVE-2023-21830 • - - - CVE-2023-21843 • • • • Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Spring Boot with GraalVM Native Image](https://bell-sw.com/blog/spring-boot-with-graalvm-native-image-performance-compatibility-migration/): Spring Boot 3.0.0-M5 was released in September. It includes numerous essential improvements, including the baked-in support for GraalVM Native Image. Although Native Image technology is gaining popularity, many developers are cautious about using it with their Spring Boot applications. In this article, we look into what needs to be configured and how to avoid migration issues with native images. Table of Contents What’s new in Spring Boot 3.0? Benefits of native images Building native images from Spring boot apps Installing Liberica NIK Creating a Spring Boot project Migration to Native Image: what could go wrong Practical guide to solving compatibility issues Building a simple Spring Boot application Issues with Java Reflection in Native Image A more complex Reflection case Porting the old school code to Spring Conclusion What’s new in Spring Boot 3.0? Spring Boot 3.0 is the first major version in 4.5 years and the first Spring Boot GA (general availability) version that supports Spring Framework 6.0 and GraalVM. The release contains 44 new features and more than a hundred other enhancements and bug fixes. Let us highlight the most noteworthy changes: GraalVM Native Image support. The initial support for native images was added to Spring Boot in the form of Spring Native, released as a beta in March 2021. Starting with Spring Boot 3.0, native support has moved to General Availability, meaning that support for GraalVM Native Image supersedes the Spring Native project. You can now convert Spring Boot apps into native executables using the standard Maven or Gradle plugins without special configuration. Java 17 baseline. Spring Boot 3.0 requires JDK 17 to run, so you need to upgrade your JDK version if you use Java LTS 8 or 11. Migration to Jakarta EE 9. A bit of background information: Oracle transferred the rights to Java EE (a set of specifications extending the Java SE with enterprise features) to the Eclipse Foundation in 2017. Java EE was subsequently renamed Jakarta EE. Because of that the package namespace changed from javax.* to jakarta.*, so you need to adjust the imports for the code to work correctly. Observability enhancement. The observability support went GA in this Spring Framework release. It is now possible to record application metrics and implement tracing with Micrometer (also received numerous updates) and its extension, Micrometer Tracing. Improved auto-configuration. The Spring Data JDBC auto-configuration has become more flexible: some auto-configured beans are now conditional and can be replaced by defining a bean of the same type. In addition, several features were removed, deprecated, or substituted with newer functionality. As it is a major release, the migration will take some effort. First of all, you need Java 17+ to work. Secondly, if you are using older Spring Boot versions, it is recommended to migrate to Spring Boot 2.7 and then move to version 3.0 following the migration guide. Make sure that the third-party dependencies you use have Jakarta EE 9 compatible versions. Lastly, check for deprecated features — they will be removed in the next major release, so prepare your project in advance. As for the native images, there are two ways to use the GraalVM functionality: Through buildpacks With a Native Build tool Liberica Native Image Kit is a GraalVM-based native-image compiler used by default with Cloud Native Buildpacks. It is also recommended by Spring as a Native Build tool. For your convenience, we implemented several methods of installing Liberica NIK on your machine. You can download it directly from our website, pull it from Docker Hub and Linux repositories, or utilize package managers or REST API. Liberica NIK is compatible with a wide variety of system configurations and is always based on the latest release of Liberica JDK (11 or 17) and GraalVM (21 or 22) with security patches, bug fixes, and other enhancements. Benefits of native images Native image is a platform-dependent standalone executable of a Java application. It contains classes of the application, dependencies, runtime, and statically linked code from the JDK. It doesn’t require a JVM but includes components from Substrate VM — Graal’s framework for compiling Java programs into self-contained executables. Native images are created using Ahead-of-time (AOT) compilation. It works like this: the compiler optimizes the code and translates it to machine code before program execution, thus eliminating the issue of slow startup. The resulting image doesn’t contain unused code and dependencies, so it requires less RAM. Consequently, native images have the following advantages: Almost immediate startup without warm-up helps to minimize cold starts and make use of such services as AWS Lambdas without affecting user experience Optimized memory consumption is optimal for dense deployments and lightweight container images Smaller attack surface due to the lack of unnecessary components enhances security Building native images from Spring boot apps Installing Liberica NIK It would be best to utilize a powerful computer with several gigabytes of RAM to work with native images. Opt for a cloud service provided by Amazon or a workstation so as not to overload the laptop. We will be using Linux bash commands further on because bash is a perfect way of accessing the code remotely. macOS commands are similar. As for Windows, you can use any alternative, for instance, bash included in the Git package for Windows. Download Liberica Native Image Kit for your system. Choose a Full version for our purposes. Unpack tar.gz with tar -xzvf ./bellsoft-liberica.tar.gz Check that Liberica NIK is installed: java -version openjdk version "17.0.5" 2022-10-18 LTS OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS) OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode, sharing) You can then use Maven commands with the JAVA_HOME= prefix. If you get the error "java: No such file or directory" on Linux, you installed the binary for Alpine Linux, not Linux. Check the binary carefully. Creating a Spring Boot project The easiest way to create a new Spring Boot project is to generate one with Spring Initializr. Select Java 17, Maven, JAR, and Spring SNAPSHOT-version (3.0.2 at the time of writing this article), then fill in the fields for project metadata. We don’t need any dependencies. Build the project and verify that everything is working: time java -jar ./target/native-image-demo-0.0.1-SNAPSHOT.jar real 0m1.404s user 0m4.883s sys 0m0.169s It takes only a second for Spring to start up, but we can do even better with GraalVM Native Image. Let’s build the project with the following command: JAVA_HOME= ./mvnw -Pnative native:compile The resulting native image is in the target directory. Check how fast the native image starts up: time /home/username/test/native-image-demo/target/native-image-demo real 0m0.026s user 0m0.013s sys 0m0.013s The startup is several times faster than with a standard Spring JAR. Migration to Native Image: what could go wrong The AOT compilation happens under the closed-world assumption,i.e., the bytecode and application dependencies that can be called at runtime must be known to the AOT compiler at build time. The compiler puts only reachable methods into the native executable. As a result, applications may behave unexpectedly or throw runtime errors. The following features must be specified in the configuration file at build time, or else the native image builder produces a fallback file at runtime: Dynamic class loading Reflection Dynamic proxy JNI Serialization In addition, debugging and monitoring is impossible with the JVM tools, so you need native debuggers and monitoring tools. The invokedynamic instruction and method handles are not supported by native image, except for cases when invokedynamic is generated by javac for some instances, such as lambda expressions, because there are no changes to called methods at runtime. For more information on feature compatibility, refer to the official GraalVM Reference manual. Practical guide to solving compatibility issues Building a simple Spring Boot application Let us first analyze the possible compilation issues using a standard JAR file, and after that, we will move to Spring Boot. First, we will create a simple HelloWorld app and verify that native image functions properly. Below is a useful trick for writing files directly in the console — you need to put the source code between <cat >./HelloWorld.java < Usually, we use the following command for running a Java program in the console: java -cp . HelloWorld.java But there’s a traditional way of creating JAR files manually, without IDE or Maven/Gradle: javac HelloWorld.java jar -cfe HelloWorld.jar HelloWorld HelloWorld.class java -jar HelloWorld.jar native-image -jar ./HelloWorld.jar It is better to use the full packaging command in scripts and build the project in a separate directory: mkdir ./build || : javac -d build HelloWorld.java jar --create --file ./build/HelloWorld.jar --main-class HelloWorld -C build . java -jar ./build/HelloWorld.jar native-image -jar ./build/HelloWorld.jar Lastly, if you want to use your own manifest, use the following command: jar -cvfm HelloWorld.jar HelloWorldManifest.txt HelloWorld.class Issues with Java Reflection in Native Image Java Reflection most frequently causes troubles when switching to Native Image. Reflection was previously used in numerous applications, even when it wasn’t necessary. This trend has declined with the dawn of Native Image, but we still have to refactor the legacy code. All we need to do to break the program compilation is to inject a simple Reflection call. Let’s get a list of class fields: import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class HelloWorld { private String field = "field example"; public static void main(String[] args) { Field[] fields = HelloWorld.class.getDeclaredFields(); List names = getFieldNames(fields); System.out.println(Arrays.toString(names.toArray())); } private static List getFieldNames(Field[] fields) { List fieldNames = new ArrayList<>(); for (Field field : fields) fieldNames.add(field.getName()); return fieldNames; } } If we run the app with a standard java -jar HelloWorld.jar, we will get the expected [field]. But if we try using Native Image, we will get the following error: Warning: Reflection method java.lang.Class.getDeclaredFields invoked at HelloWorld.main(HelloWorld.java:10) We can tell Native Image, which fields we are going to read: cat >./reflect-config.json < It seems to have solved the problem, but what should we write in the configuration file? And does it mean we have to manually define every access point? Gladly, we have Java Agent that can perform this task automatically. We should simply run the app with the Agent and feed the result to Native Image: java -agentlib:native-image-agent=config-output-dir=./config -jar ./build/HelloWorld.jar native-image -H:ConfigurationFileDirectories=./config -jar ./build/HelloWorld.jar The program will successfully compile and give out the desired result thanks to the config directory now having all the files we otherwise would have to state manually: jni-config.json predefined-classed-config.json proxy-config.json reflect-config.json resource-config.json serialization-config.json A more complex Reflection case The Agent writes only those access points that were used during app execution. Suppose we have an application that utilizes Reflection differently in various cases. In the example below, there are two paths depending on the parameter value, 1 or 2. public class HelloWorld2 { public class A { private String field1 = "field 1"; } public class B { private String field2 = "field 2"; } public static void main(String[] args) { Class cls = null; if (args.length < 1) { return; } String selector = args[0]; switch(selector) { case "1": { cls = A.class; break; } case "2": { cls = B.class; break; } } if (null != cls) { Field[] fields = cls.getDeclaredFields(); List names = getFieldNames(fields); System.out.println(Arrays.toString(names.toArray())); } } private static List getFieldNames(Field[] fields) { List fieldNames = new ArrayList<>(); for (Field field : fields) fieldNames.add(field.getName()); return fieldNames; } } Let’s try to compile the app with Native Image: mkdir ./build || : javac -d build HelloWorld2.java jar --create --file ./build/HelloWorld2.jar --main-class HelloWorld2 -C build . java -jar ./build/HelloWorld2.jar native-image -jar ./build/HelloWorld2.jar The result will be: Warning: Reflection method java.lang.Class.getDeclaredFields invoked at HelloWorld2.main(HelloWorld2.java:34) Warning: Aborting stand-alone image build due to reflection use without configuration. Failed generating 'HelloWorld2' after 15.9s. Generating fallback image... Warning: Image 'HelloWorld2' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary). The error message is quite explicit. We have the HelloWorld2 binary, but we can’t use it without JDK. The Agent creates the following configuration: java -agentlib:native-image-agent=config-output-dir=./config2 -jar ./build/HelloWorld2.jar cat ./config2/reflect-config.json The config file contains []. In other words, it is empty because the Agent didn’t find any Reflection. We need to run it several times adding different execution branches. There is a config-merge-dir option to merge multiple results of config generation: java -agentlib:native-image-agent=config-merge-dir=./config2 -jar ./build/HelloWorld2.jar 1 java -agentlib:native-image-agent=config-merge-dir=./config2 -jar ./build/HelloWorld2.jar 2 We now have more sound data in the ./config/reflect-config.json file: [ { "name": "HelloWorld2$A", "allDeclaredFields": true }, { "name": "HelloWorld2$B", "allDeclaredFields": true } ] Let’s compile the binary: native-image -H:ConfigurationFileDirectories=./config2 -jar ./build/HelloWorld2.jar The result is: Produced artifacts: /home/username/test/ni/HelloWorld2 (executable) /home/username/test/ni/HelloWorld2.build_artifacts.txt (txt) ==================================================================================================== Finished generating 'HelloWorld2' in 29.8s. How did GraalVM understand that the config file contents fully cover all Reflection calls? Could there be a call that we missed? Let’s add one more class and execution path: public class C { private String field3 = "field 2"; } case "3": { cls = C.class; break; } Try to compile an image: Produced artifacts: /home/username/test/ni/HelloWorld2 (executable) /home/username/test/ni/HelloWorld2.build_artifacts.txt (txt) ==================================================================================================== Finished generating 'HelloWorld2' in 30.3s. Something went wrong! Native Image thinks it’s the right configuration, but it’s not — we made it wrong intentionally. So it must break. How? username@server:~/test/ni$ ./HelloWorld2 1 [field1, this$0] username@server:~/test/ni$ ./HelloWorld2 2 [field2, this$0] username@server:~/test/ni$ ./HelloWorld2 3 [] The code was executed without any errors or segfaults, but the result is wrong. It should be [field3, this$0] instead of []. The getDeclaredFields method will always give the empty list. We can solve the issue only through recompilation. The above example shows perfectly well the issues the developers will have to face if they wish to utilize GraalVM. It seems easy in theory, but the devil is in the details. Porting the old school code to Spring First of all, let’s make our Spring Boot app, which we created in the “Building native images” section, run some meaningful code similar to our console HelloWorld program: package sample.username.nativeimagedemo; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class HelloWorld { @EventListener(ApplicationReadyEvent.class) public void doSomethingAfterStartup() { System.out.println("Hello World!"); } } If we run this application, it will successfully print out “Hello World!” both as a JVM-based version and as a binary. The next step is to port our application printing out the class fields to Spring: @Component public class HelloWorld { private String field = "field example"; public static void run() { Field[] fields = HelloWorld.class.getDeclaredFields(); List < String > names = getFieldNames(fields); System.out.println(Arrays.toString(names.toArray())); } private static List < String > getFieldNames(Field[] fields) { List < String > fieldNames = new ArrayList < > (); for (Field field: fields) fieldNames.add(field.getName()); return fieldNames; } @EventListener(ApplicationReadyEvent.class) public void doSomethingAfterStartup() { run(); } } After running the code with JVM, we get the expected result. NativeImageDemoApplication : No active profile set, falling back to 1 default profile: "default" NativeImageDemoApplication : Started NativeImageDemoApplication in 0.671 seconds (process running for 1.055) [field] But all is not well with Native Image: username@server:./mvnw -Pnative clean package username@server:/home/username/test/native-image-demo/target/native-image-demo Started NativeImageDemoApplication in 0.014 seconds (process running for 0.017) [] The result is similar to the one we got with the console program after creating different execution paths without adding some of them to the config file. The output is [], which means that Native Image has no access to the class fields through Reflection and therefore thinks that these fields do not exist. At the same time, we don’t get an explicit error message like with the console program. Spring can magically make the app compile, but without guarantees of correct execution. Let’s create the reflect-config.json file in the root: [ { "name": "sample.username.nativeimagedemo.HelloWorld", "allDeclaredFields": true } ] After that, include it into the native-maven-plugin parameters: org.graalvm.buildtools native-maven-plugin -H:ReflectionConfigurationFiles=reflect-config.json Recompile the project: username@server:./mvnw -Pnative clean package username@server:/home/username/test/native-image-demo/target/native-image-demo Started NativeImageDemoApplication in 0.03 seconds (process running for 0.038) [field] The native-image parameters now go through the plugin configuration. How do we generate config files with access points? The easiest way is to run JAR on JVM the old school way and generate the directory with the config files. ./mvnw clean package java -agentlib:native-image-agent=config-output-dir=./config -jar /home/username/test/native-image-demo/target/native-image-demo-0.0.1-SNAPSHOT.jar Include the file into build parameters: org.graalvm.buildtools native-maven-plugin -H:ConfigurationFileDirectories=./config Recompile the project: username@server:./mvnw -Pnative clean package username@server:/home/username/test/native-image-demo/target/native-image-demo Started NativeImageDemoApplication in 0.029 seconds (process running for 0.038) [field] In theory, we can automate the process with Maven. But in reality, we don’t need to do that because later on we will have to introduce changes into those files manually. Conclusion Spring Boot 3 can now compile native images out of the box. But we have to understand that even if Spring can magically compile the app without errors, there are no guarantees it will function properly. Spring Boot development with Native Image is an exciting but extensive field for Spring developers to master. In this article we evaluated only a single case of resolving the issues associated with migration to Native Image. Subscribe to our newsletter, so you don’t miss other guides on migrating Spring Boot apps to Native Image with Liberica NIK. - [Liberica Native Image Kit 22.3.1 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-22-3-1-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 22.3.1 as part of Critical Patch Update (CPU) release cycle. The builds contain several security fixes and enhancements. Note that Liberica NIK releases are aligned with GraalVM release schedule, which underwent essential changes in 2023. GraalVM feature releases were previously issued every three months. Starting with JDK 20 release in March 2023, GraalVM CE will conform to the six-month JDK release cadence. CPU builds will see the light four times a year as before. New release calendar is as follows. For more information, consult the GraalVM website. Date Version Type Name January 24, 2023 22.3.1 CPU March 21, 2023 23.0.0 Feature GraalVM for JDK 20 April 18, 2023 23.0.1, 22.3.2 CPU July 18, 2023 23.0.2, 22.3.3 CPU September 19, 2023 23.1.0 Feature GraalVM for JDK 21 All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Summary of fixes and enhancements Important changes We enabled the cross-compilation of musl-based Linux static images. Static native images are created by statically linking against musl, a lightweight C library implementation. They are smaller in size and start up faster. The native-image can produce both static and dynamic musl-based images. List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-21835 5.3 security-libs javax.net.ssl network low none none unchanged none none low CVE-2023-21830 5.3 other-libs - network low none none unchanged none low none CVE-2023-21843 3.7 client-libs javax.sound network high none none unchanged none low none Conclusion BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [Distroless containers for security and size?](https://bell-sw.com/blog/distroless-containers-for-security-and-size/): Distroless container images are becoming increasingly popular in cloud-native computing because they make it possible to create both secure and lightweight production container images. Two key benefits of distroless are reduced disc space usage in the clusters and minimized attack surface. But are they truly the optimal solution for your application? Let’s explore. Table of Contents What is a Distroless Container Image 6 questions about distroless and small Linux distros 1. “Is there really no distribution in distroless?” 2. “Are distroless smaller than traditional Docker images?” 3. “How to debug and configure distroless Docker images effectively?” 4. “Are distroless more secure than regular Docker images?” 5. “Can I make my own distroless image?” 6. “What is better, a distroless container image or a small Linux distribution?” How to choose a small Linux distribution How to use distroless with Java Distroless vs Liberica Runtime Container: compare image sizes Conclusion What is a Distroless Container Image Distroless image is a type of Docker image that contains only the essential components for running the application. This approach is in line with the modern practices of deploying the application to the cloud in an immutable container, meaning that we have no need for introducing system changes or utilizing libraries not involved in app’s work. 6 questions about distroless and small Linux distros 1. “Is there really no distribution in distroless?” To set the record straight — distroless images are not distroless. Although they are served up as ones containing only the application and runtime dependencies without an OS, the only genuinely distroless images are built from scratch. Such OS-less scratch images can be used to run simple statically linked hello-world apps. But the absence of an OS deems running any enterprise-grade application impossible, as there are no Certificate data for proper TLS management; libc for dynamically compiled languages; Necessary components for regular work: timezone, passwd, group folders, etc. So distroless images do contain a Linux distribution, albeit an incredibly stripped-down one, without a package manager, shell, or other typical Linux components. 2. “Are distroless smaller than traditional Docker images?” The fact the application relies on the operating system makes it hard to keep the image slim. Let’s take a look at the most popular distroless images produced by GoogleContainerTools (the data is valid as of July 25, 2024): gcr.io/distroless/static-debian12 — 1.9 MB — includes ca-certificates, timezone data, a etc/passwd entry, and a /tmp directory. Statically-linked applications will benefit from this image, but what if your application has dynamic features and requires libc? gcr.io/distroless/base-debian12 — 29.7 MB — is built upon the static image and includes glibc, libssl, and openssl. The image is optimal for dynamically-linked programs, but even in this case, you may require additional shared libraries, which takes us to another level (or layer, for that matter). gcr.io/distroless/cc-debian12 — 32.3 MB — is built upon the base image and includes libgcc1 with dependencies. What about interpreted or VM-based languages (Java, Python, JavaScript)? gcr.io/distroless/java21-debian12 — 192 MB — includes a base Linux image plus OpenJDK (Eclipse Temurin) with dependencies. The more dynamic your application is, the more OS libraries it needs, so the image size bloats. Distroless image size 3. “How to debug and configure distroless Docker images effectively?” Due to the absence of shell access, troubleshooting or profiling distroless images requires mastering additional technologies or techniques. One efficient approach is to use ephemeral containers. These containers can be started at an arbitrary point in time and run temporarily to accomplish some task. They contain shell access and necessary tools for the task at hand: debugging utilities, a profiler, etc. Ephemeral containers are useful even if you don’t use distroless images, because They enable you to keep that target container clean of auxiliary tools, You don’t have to run the target container with administrator access, which may be unacceptable from the security standpoint. Ephemeral containers are part of the Kubernetes API, but you can easily spin them up without Kubernetes. For more details, refer to this guide on using ephemeral containers to profile containerized Java applications. As for the configuration of distroless images, it might be challenging. To adjust Google's images, you need to know bazel, and adding new packages is complicated without a package manager. A limited range of out-of-the-box distroless images increases the risk of the image being incompatible with your app. If you don’t want to use distroless images, you can save resources by selecting an optimal Linux base image (some modern OSs are similar to or smaller than distroless/base) or mastering at least one Docker container image reduction technique. 4. “Are distroless more secure than regular Docker images?” It is believed that distroless images are more secure due to smaller attack surface. The attack surface is a sum of attack vectors, the potentially exploitable paths or scenarios. In other words, it is not about the number of files in your container, but how accessible and vulnerable they are to attacks. Such components as config files, locale, man pages. etc., are less likely to contribute to the attack surface than files sitting in the direct execution path: web servers, libc, encryption modules, etc. Distroless container images contain a very scarce number of Linux components, including those that might be more accessible to attackers, so they have a smaller track surface than regular Linux distros. But still, there are a few factors to consider if you want to increase the security of your containers: Distroless images are still derived from a Linux distribution. Therefore, they can have nested vulnerabilities. A missed CVE can bear a significant risk of exploits. Software standardization contributes to enhanced security and accelerated remediation, but it might be challenging with distroless images. The inability to use the same distroless image with the same Linux distro and version for all workloads increases the attack surface. Apart from Linux, the dependencies you use for your project and the runtime for VM-based languages are also associated with security risks. For instance, if you run Java-based workloads, you have to be sure that the OpenJDK distribution in your base image is updated just as regularly as Linux and includes all the latest security patches. Summarizing, distroless container images have a smaller attack surface. But it is not enough to simply migrate to distroless. To enhance security, regular software updates, standardization, and implementation of modern software security mechanisms such as a Software Bill of Materials (SBOM) are essential to keep your project clean of known vulnerabilities and react to security threats in a timely fashion. 5. “Can I make my own distroless image?” If you need a specific distroless container image not supported by providers of distroless images such as Google or Chainguard, you can make your own. To do that, you can use bazel or apko. For more details on building your own distroless images, refer to this article. 6. “What is better, a distroless container image or a small Linux distribution?” As usual, there’s no one-size-fits-all solution. Distroless may be a perfect solution for some enterprises. They have a good security profile, and if your application fits into the static distroless image, you can reduce disk space usage and network traffic in your clusters. However, there is an alternative to distroless container images: minimalistic Linux distributions such as Alpine or Alpaquita. Their base image size is only about 3 MB, but they preserve all core functionalities of Linux. These distributions contain only necessary packages, and others can be pulled from the repository. As a result, they provide flexible, configurable environments for virtualization or containers. If for some reason, you don’t want to use distroless images, consider a small Linux distribution that provides a familiar workflow. How to choose a small Linux distribution In the section above, we mentioned Alpine and Alpaquita as two minimalistic Linux distros perfect for building small container images. What are the differences between them? Alpine Linux is a community project, and Alpaquita Linux has both community and LTS versions with technical support from BellSoft. Alpine is based on musl libc, and Alpaquita Linux has versions with both musl and glibc. The glibc-based version is only a few megabytes bigger than the musl-based Alpaquita (3.5 with musl vs 11.5 with glibc). In addition, Alpaquita supports musl perf, an optimized musl implementation to meet or exceed the glibc performance. The comparison of key Alpine and Alpaquita features can be summarized as follows: Feature Alpaquita Alpine Free community support ✓ ✓ Commercial support ✓ – Open source ✓ ✓ musl support default musl musl perf default musl glibc support ✓ – License EULA GPL2 mostly, MIT for musl, various licenses for software components Platform support x86_64 AArch64 x86x86_64 AArch64 ARMhf ARMv7 ppc64le s390x GraalVM support ✓ – Production-ready Java containers ✓ – The picture below shows the comparison of the most widely used Linux distros in terms of size. Linux container image size And here is the comparative graph for base Docker images with and without a JDK: Linux Docker image size with and without JDK As you can see, you can have a minuscule yet fully functional Linux that can be tailored to your application without inflating the Docker container image. To make Alpaquita an optimal choice for enterprise applications, we provide LTS Alpaquita releases with a strict update schedule, Security Advisory, and 24/7 commercial support. How to use distroless with Java If you decide to try distroless out with your Java application, here’s how you can do that. We will take Spring Petclinic as an example, but you can use your own project. Copy the following Dockerfile: FROM eclipse-temurin:21.0.3_9-jdk-alpine as builder WORKDIR /home/app ADD spring-petclinic-main /home/app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package FROM gcr.io/distroless/java21-debian12 WORKDIR /home/app COPY --from=builder /home/app/spring-petclinic-main/target/*.jar petclinic.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/petclinic.jar"] We use Docker multi-stage builds to build the jar file and then place it into the fresh image. This way, the final image will be free of unnecessary layers. It is not possible to build an application using a distroless image because shell is needed for that. So, at the first stage, we take a base image with Eclipse Temurin and Alpine. At the final stage, we take Google’s distroless image for Java based on Eclipse Temurin 21 and Debian. Build the image with the usual command: docker build -t petclinic-distroless . Now, check the image size: docker images REPOSITORY TAG IMAGE ID CREATED SIZE petclinic-distroless latest ebb8f2b96dd6 53 seconds ago 263MB Before we make any conclusions, let’s build another image using Liberica Runtime Container. Distroless vs Liberica Runtime Container: compare image sizes BellSoft’s Liberica Runtime Container is based on Liberica JDK Lite optimized for cloud and Alpaquita Linux. We will need the following Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder WORKDIR /home/app ADD spring-petclinic-main /home/app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package FROM bellsoft/liberica-runtime-container:jre-21-musl WORKDIR /home/app COPY --from=builder /home/app/spring-petclinic-main/target/*.jar petclinic.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/petclinic.jar"] Here, we build the project using the base image with Liberica JDK. After that, we copy the resulting jar to a fresh container with Liberica JRE. Build the image and check its size: docker build -t petclinic-alpaquita . docker images REPOSITORY TAG IMAGE ID CREATED SIZE petclinic-alpaquita latest 325ba5c52e28 7 seconds ago 200MB As you can see, an image based on Liberica Runtime Container is 63 MB smaller than the distroless-based one! Conclusion To sum up, distroless is not a one-size-fits-all solution. Some applications will run with distroless/static, while others may be a perfect fit for distroless/base. In some cases, the complicated workflow and constant image monitoring for vulnerabilities may outweigh the possible pros of distroless. A viable alternative to distroless images is to optimize the Docker images by reducing the OS layer and cutting the runtime for VM-based apps. As a result, distroless images and lightweight Linux distributions for the cloud such as Alpaquita Linux offer excellent opportunities to optimize your containers. Whether your goal is to enhance security, reduce size, or standardize the environments, these tools can make a difference. Visit our Docker Hub repository, choose an image, and start building better containers today! Go to Alpaquita Linux Docker Hub repository - [Static images with Liberica Native Image Kit](https://bell-sw.com/blog/static-images-with-liberica-native-image-kit/): Java is not that slow and memory devouring as it used to be 20 years ago, but there’s still room for improvement. If you are looking for ways to accelerate your Java apps and reduce the size of container images, GraalVM is at your service! The latest CPU release of Liberica Native Image Kit (NIK), a GraalVM-based utility for native images generation, includes an exciting new feature, namely the support for cross-compilation of musl-based Linux static images. Read on to know how static images will make your Java apps run at rapid-fire pace! Benefits of static images Static images are binaries statically linked against a C library: musl, glibc, or any other libc implementation. Static images have several essential advantages: Unmatched flexibility when choosing a base Docker image because they work even with FROM scratch base images, the only true distroless images that have always been unworkable for naturally dynamic Java applications; Accelerated startup due to the fact that no symbol resolution is performed at run time by the dynamic linker. Note that with Liberica NIK, you can produce both static and dynamic images. Static images can sometimes be even smaller than dynamic ones, but everything depends on your application. Benefits of musl libc The most popular C library implementations at the heart of any Linux distribution are glibc and musl. glibc is the most widespread libc with great compatibility and optimal performance. But with 35 years of history behind it, its codebase is bloated and associated with significant overhead. musl has gained popularity in recent years as a minimalistic C library implementation perfect for cloud-native applications. Thanks to a much cleaner codebase, it has Small static and dynamic overhead leading to lower memory consumption; Lesser attack surface making container images more secure. Two lightweight Linux distributions, Alpine and Alpaquita, are based on musl and help to reduce the size of Docker container images drastically. Cross-сompiling musl-based images Let’s now build a static image using Liberica NIK. Installing NIK If you are new to Native Image, download Liberica NIK version 22.3.1 with support for cross-compilation. Follow the instructions to complete the installation. Installing Cross Toolchain You need a musl-based gcc toolchain for cross-compilation. You can get it here (note that later versions might not work). Extract the toolchain to a selected directory (referred to as $TOOLCHAIN_DIR later on). Installing zlib Unfortunately, this toolchain does not contain libz.a, which is needed for native compilation. The easiest way is to copy it from your host system: install a zlib development package (on Ubuntu, it is called zlib1g-dev), then copy the installed libz.a into $TOOLCHAIN_DIR/x86_64-linux-musl/lib directory. Alternatively, you can build the library from source. Download a source package from the official zlib website, unpack it, change into the zlib directory, and run CC=$TOOLCHAIN_DIR/bin/x86_64-linux-musl-gcc ./configure --prefix=$TOOLCHAIN_DIR --static make make install Building Static Native Image Everything is ready for cross-compilation! Make sure your toolchain’s bin directory is in PATH by running: x86_64-linux-musl-gcc You should get the output similar to: x86_64-linux-musl-gcc is /…/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc First, let’s compile a regular glibc-based static image: native-image --static -jar app.jar app-static-glibc Now, a musl-based one: native-image --static --libc=musl -jar app.jar app-static-musl Finally, compare image sizes (your numbers will be different): ls -l app-static-* -rwxrwxr-x 1 user user 14114944 Jan 30 12:43 app-static-glibc -rwxrwxr-x 1 user user 12208600 Jan 30 12:58 app-static-musl The musl-based image is about 2 Mb, which is 13% smaller than a glibc-based one. - [How to build Linux containers with Docker](https://bell-sw.com/blog/how-to-use-linux-containers/): A containerization tutorial based on Alpaquita Stream Linux containers are a powerful solution for Software standardization; Acceleration of development and testing; Effective resource management throughout the whole lifecycle of an application. We have already discussed the intricacies behind the technology, its advantages, and differences from virtualization in our previous article dedicated to JVM in Linux containers. Here, we will provide a step-by-step guide to building Linux containers with applications intended for Cloud deployment. As an example, we will use BellSoft’s open-source Alpaquita Stream containers hosted on the Docker Hub Container Image Library. To complete the guide, you must have the docker daemon installed and running. We will explain how to pull a selected container image from the repository and run it with Java, Python, GCC-based applications, and native images. You can use your own app or create a sample project as shown below. Table of Contents Linux containers for Java applications Pull a Docker image Write a Dockerfile and run the app Linux containers for Native Image Pull a Docker image Create a native image Linux containers for Python Linux containers for GCC Conclusion Linux containers for Java applications Pull a Docker image BellSoft provides an extensive selection of Alpaquita Linux images: consult the overview to select an appropriate solution for your application. To create a container, run $ docker image pull docker bellsoft/: Start the container with $ docker run -it --rm bellsoft/: Alternatively, utilize a combined command: $ docker container run --rm -it bellsoft/: Suppose you have chosen a Liberica Runtime Container, which includes musl-based Alpaquita Stream (a glibc-based option is also available) and Liberica JDK Lite 11. The command will be as follows: $ docker container run --rm -it bellsoft/liberica-runtime-container:jdk-11.0.17-musl Note that pulling an image is not obligatory, but recommended if you don’t want to pull an image every time you repeat the build process. Write a Dockerfile and run the app By using a JRE image instead of a full JDK to run a Java application, you will reduce the container size by approx. 50%. So in the Dockerfile for our application, we will specify two images — one for building an app, and another for running it. We will use a standard Spring Petclinic project as a sample. Write the following Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-17-stream-musl as builder WORKDIR /home/myapp RUN apk add git RUN git clone https://github.com/spring-projects/spring-petclinic.git RUN cd spring-petclinic && ./mvnw package FROM bellsoft/liberica-runtime-container:jre-17-stream-musl WORKDIR /home/myapp COPY --from=builder /home/myapp/spring-petclinic/target . CMD ["java", "-jar", "spring-petclinic-3.0.0-SNAPSHOT.jar"] Build the app image: $ docker build --progress plain -t javaappimage . Now. run the the image: docker run -p 8080:8080 javaappimage Done! Open the browser at http://localhost:8080/ to access the Petclinic application. Linux containers for Native Image Pull a Docker image We provide images with Liberica Native Image Kit (NIK), an open-source GraalVM-based utility for converting JVM applications into native executables. Developers who work with native images or consider integrating the technology into their project can pull the following image: $ docker container run --rm -it bellsoft/liberica-native-image-kit-container:jdk-11-nik-21.3.3-stream-musl Or any image you deem appropriate depending on the JDK and NIK version and libc implementation (BellSoft’s optimized musl or glibc). Create a native image First, let’s write a simple application to be converted into a native executable. Alternatively, use your own project. public class Example { public static void main(String[] args) { String str = "Native Image is awesome"; String reversed = reverseString(str); System.out.println("The reversed string is: " + reversed); } public static String reverseString(String str) { if (str.isEmpty()) return str; return reverseString(str.substring(1)) + str.charAt(0); } } The Dockerfile must contain the following information: FROM bellsoft/liberica-native-image-kit-container:jdk-11-nik-21.3.3-stream-musl WORKDIR /home/myapp COPY Example.java /home/myapp RUN javac Example.java RUN native-image Example FROM bellsoft/alpaquita-linux-base:stream-musl WORKDIR /home/myapp COPY --from=0 /home/myapp/example . CMD ["./example"] Go to the application directory and run $ docker build . Building the image [+] Building 370.9s (14/14) FINISHED => [internal] load build definition from Dockerfile 0.1s => => transferring dockerfile: 357B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/bellsoft/alpaquita-linux-base:stream-musl 0.0s => [internal] load metadata for docker.io/bellsoft/liberica-native-image-kit-container:jdk-11-nik-21.3.3-stream-musl 4.8s => [internal] load build context 0.1s => => transferring context: 454B 0.0s => [stage-1 1/3] FROM docker.io/bellsoft/alpaquita-linux-base:stream-musl 0.1s => [stage-0 1/5] FROM docker.io/bellsoft/liberica-native-image-kit-container:jdk-11-nik-21.3.3-stream-musl@sha256:3c5a09abb2559cf0 293.4s => => resolve docker.io/bellsoft/liberica-native-image-kit-container:jdk-11-nik-21.3.3-stream-musl@sha256:3c5a09abb2559cf04e7527b81b 0.0s => => sha256:3c5a09abb2559cf04e7527b81bfa123ca52592ef2bce1512d837fbd0336440e9 951B / 951B 0.0s => => sha256:79998423fd609bb42881f8eb6721aa22451cdb46363b4fc5f49fc50390759138 2.72kB / 2.72kB 0.0s => => sha256:4f4293381c77f4d905cfa2034b4fe3adff4515508d08b0093a8db16728ba4879 3.34MB / 3.34MB 7.1s => => sha256:c3263c13b6f5c1ba5a386dc02047af54fdeefc8a92538ffc973915cdb0de9881 532.42kB / 532.42kB 7.0s => => sha256:f6c678d36153293dc6b01a73696b4ef60a9ecb44e62da72fd96b31e679f1ed2d 323.95MB / 323.95MB 284.4s => => extracting sha256:4f4293381c77f4d905cfa2034b4fe3adff4515508d08b0093a8db16728ba4879 0.1s => => extracting sha256:c3263c13b6f5c1ba5a386dc02047af54fdeefc8a92538ffc973915cdb0de9881 0.0s => => extracting sha256:f6c678d36153293dc6b01a73696b4ef60a9ecb44e62da72fd96b31e679f1ed2d 8.8s => [stage-1 2/3] WORKDIR /home/myapp 0.0s => [stage-0 2/5] WORKDIR /home/myapp 0.5s => [stage-0 3/5] COPY Example.java /home/myapp 0.0s => [stage-0 4/5] RUN javac Example.java 1.0s => [stage-0 5/5] RUN native-image Example 70.6s => [stage-1 3/3] COPY --from=0 /home/myapp/example . 0.1s => exporting to image 0.1s => => exporting layers 0.1s => => writing image sha256:f03df73515790f3ffe210fc139d5a0752c088fd242f571e19d111377f7703c6a 0.0s Verify the image was created. $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE f03df7351579 About a minute ago 19MB Tag the newly built image with a meaningful name. $ docker tag f03df7351579 bellsoft-nik:example-musl $ docker run -it --rm f03df7351579 The reversed string is: emosewa si egamI evitaN Linux containers for Python BellSoft provides Alpaquita Linux images with Python 3.10 and basic Python utilities (pip, setuptools, wheel). Two libc options (BellSoft’s musl perf and glibc) are available. First, let’s write a simple flask application: $ cat requirements.txt flask markup jinja2 $ cat test.py from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello, Docker!' Now, write a Dockerfile with the following contents: FROM bellsoft/alpaquita-linux-python:3.10-stream-musl WORKDIR /myapp COPY requirements.txt . RUN pip3 install -r requirements.txt COPY test.py . ENV PATH=/root/.local:$PATH CMD ["python3", "-m" , "flask", "--app", "./test.py", "run", "--host=0.0.0.0"] Finally, build a Docker image with this Dockerfile: $ docker build -t flaskimage . Building an image [+] Building 1.3s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 38B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/bellsoft/alpaquita-linux-python:3.10-stream-musl 1.2s => [internal] load build context 0.0s => => transferring context: 63B 0.0s => [1/5] FROM docker.io/bellsoft/alpaquita-linux-python:3.10-stream-musl@sha256:2d5656810f19f028b5992cbfcdd3e833cfb1839b5b6078b5a2a1 0.0s => CACHED [2/5] WORKDIR /myapp 0.0s => CACHED [3/5] COPY requirements.txt . 0.0s => CACHED [4/5] RUN pip3 install -r requirements.txt 0.0s => CACHED [5/5] COPY test.py . 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:9f9335b2d7339a656e1b860e0d0d293c528e06aa21d0be41ee46b67fbb9e9f12 0.0s => => naming to docker.io/library/flaskimage 0.0s You can now run your Python application in Docker: $ docker run -d -p 6000:5000 flaskimage 630ae5583186bf0a5d99c97bca68b41401cf6b4b1d22f770fa4f757d23dc240e $ curl http://127.0.0.1:6000 Hello, Docker! Linux containers for GCC BellSoft provides Alpaquita Linux images with the GCC compiler version 12.2, tools, and libraries for development in C/C++. Two libc options (BellSoft’s musl perf and glibc) are available. We will use a C++ example to show that our gcc image could be used for building both C++ and C projects. Our gcc images include the following packages that are not obligatory, but could be useful for building. gcc packages autoconf automake bash binutils bzip2 bzip2-dev curl curl-dev diffutils file findutils fortify-headers g++ gawk gcc gdb git grep hexdump jpeg-dev krb5-dev libevent-dev libffi-dev libjpeg-turbo-dev libpng-dev libtool libxml2-dev libxslt-dev make ncurses-dev openssl-dev patch pkgconf readline-dev sed subversion unzip xz xz-dev yaml-dev zlib-dev First, let’s write a simple C++ program: $ cat hello.cpp #include using namespace std; int main() { cout << "Hello World" << endl; return 0; } In the Dockerfile, specify an Alpaquita image for building an app, and a base image, which will be used for working with the container further on: FROM bellsoft/alpaquita-linux-gcc:12.2-stream-musl as builder WORKDIR /home/myapp COPY hello.cpp . RUN g++ hello.cpp -o hello -static FROM bellsoft/alpaquita-linux-base:stream-musl WORKDIR /home/myapp COPY --from=builder /home/myapp/hello . CMD ["./hello"] Now, build the Docker container image: $ docker build -t cppappimage . Run it with the following command: $ docker run --rm cppappimage Hello World Conclusion In this article, we learned how to construct Linux containers with Java, Python, C/C++ applications, and Native Image. We used Alpaquita Stream as a base Linux distribution. As you can see, containerization with Alpaquita is fast and effortless, and in the end, you get a performant microcontainer that can be used as is or further configured for specific purposes. If you need to add additional Linux packages, take a look at a detailed APK guide. All Alpaquita packages are verified by BellSoft regarding clean licenses to eliminate the risk of license violation, meaning that Alpaquita is legally safe for enterprise use. Alpaquita Stream is free to use, but BellSoft also provides commercial support for Alpaquita Cloud Native Platform, — a solution based on Alpaquita and Liberica JDK Lite, — which includes Alpaquita LTS versions with minimum four years of bug fixes and updates; 24/7 service; Emergency patches and fixes; Support both for Linux and Java from one vendor. Click on the button below to learn more about the offer. Contact us - [Java 20 — new features and improvements](https://bell-sw.com/blog/java-20-new-features-and-improvements/): In case you are looking forward to the next LTS Java release and forgot about minor JDK versions — JDK 20 reminds of its existence! It has entered the Rampdown Phase Two, and the feature set has been frozen. Let’s summarize the JEPs targeted for JDK 20 to brush up on where Java is going. JEP 429: Scoped Values (Incubator) JEP 429: Scoped values is the only new feature introduced in JDK 20 as an Incubator API. Its goal is to facilitate thread management by sharing immutable data within and across threads. Scoped values prohibit the change of a variable by remote code, so the data can be reliably communicated to callees in the same method. Scoped values will be a great asset for developers working with virtual threads, the number of which can reach thousands or even millions. You can find more details on the JEP in a dedicated short article. JEP 432: Record Patterns (Second Preview) Record patterns were introduced in JDK 19 as a Preview feature. Record patterns are used for record values deconstruction. They promote more declarative programming and facilitate code writing because they are nestable and can be matched against and decomposed by a nested pattern. JEP 432 proposes a Second Preview with several enhancements: Support for inference of type arguments of generic record patterns; Support for record patterns to appear in the header of an enhanced for statement; Removed support for named record patterns. JEP 433: Pattern Matching for switch (Fourth Preview) JEP 433 introduces a Forth Preview of pattern matching for switch expressions and statements. The improvements include: The simplification of the grammar for switch labels; Changed type of an exception thrown when no switch label applies at run time. A switch statement or expression over an enum class throws MatchException instead of IncompatibleClassChangeError; Support for inference of type arguments for generic record patterns in switch statements or expressions. JEP 434: Foreign Function & Memory API (Second Preview) JEP 434 brings to JDK 20 a Second Preview of Foreign Function & Memory (FFM) API, first introduced in JDK 17 as Incubator API. FFM is more safe by design than Java Native Interface and provides a more robust way of working with foreign code and memory not managed by the JVM. JEP 434 includes the following enhancements: Unified MemorySegment and MemoryAddress abstractions; Improved sealed MemoryLayout hierarchy enables convenient usage of FFM with pattern matching in switch expressions and statements; Split MemorySession into Arena and SegmentScope facilitates sharing segments across maintenance boundaries. JEP 436: Virtual Threads (Second Preview) Virtual threads will bring multithreaded programming in Java to a completely different level. In short, instead of using few OS threads or diving into asynchronous programming, developers can have thousands or millions of virtual threads, which are not tied to an OS thread for the whole lifetime of the code. At the same time, they are easy to debug, monitor, or profile. To find out more about this exciting feature, read a short article dedicated to the topic. JEP 436: Virtual Threads is a Second Preview with minor changes to functionality not specific to virtual threads only. JEP 437: Structured Concurrency (Second Incubator) Structured concurrency will improve reliability and maintainability of multithreaded Java code and helps to coordinate thousands of virtual threads efficiently. With the new library for structured concurrency, developers form a tree-shaped hierarchy of tasks owned by a parent task. This limits the lifetime of subtasks within a single syntactic block and helps to prevent thread leaks or any other unexpected behavior of multithreaded code. JEP 437 updates the StructuredTaskScope to support the inheritance of scoped values by threads created in a task scope, thus streamlining the sharing of immutable data across threads. Conclusion JDK 20 goes GA in March 2023. Early-access builds are available to experiment with. The next JDK 21 will enjoy long-term support and is fit for enterprise use. So if you are planning the migration to Java 21 LTS, use JDK 20 as a shooting range to test the new functionality and see how well it goes with your current application structure! - [Top-6 frameworks for Java microservices in 2023](https://bell-sw.com/blog/top-6-frameworks-for-java-microservices-in-2023/): Cloud-native microservices are a new norm, so if you are starting an enterprise project or looking to conquer new career heights, you need an ultimate toolkit to build resilient, highly-performant Java services tailored to the Cloud. A framework provides a platform and necessary tools for you to develop applications without having to start from scratch and deal with extra logic. But there are so many frameworks on the market nowadays! How to select an optimal solution for your specific case? Should you go with good old Spring Boot or try new grounds? Let’s find out! Table of Contents Spring Boot Quarkus Micronaut Dropwizard MicroProfile Vert.x Choosing a framework is only the first step towards the Cloud Spring Boot Developer: Pivotal Software Top advantages Top disadvantages Wide variety of production-ready solutions Increased binary size Easy to solve issues with large community and detailed documentation Slower startup Nested cozily in the Spring framework ecosystem, Spring Boot is the most popular project for building microservices among Java developers. It follows the “convention over configuration” approach, which aims to reduce the number of decisions developers have to make when using the framework. As such, Spring Boot Offers numerous out-of-the-box tools for creating production-ready applications with minimal effort; Configures the application automatically based on the added dependencies; Comes with an embedded database and an HTTP server such as Tomcat, Jetty, etc., which minimizes the time required to set up an application. It also builds a single jar file with a web server, so the application is extremely easy to deploy; Supports annotations, thus eliminating the boilerplate code needed for XML configurations. A large community thriving around Spring Boot and a comprehensive up-to-date documentation enable the developers to resolve issues quickly and find a ready solution for almost any query. In addition, Spring Boot recently received baked-in support for GraalVM Native Image — developers can now convert Spring Boot apps into native executables without additional configuration. But at the same time, Spring Boot applications may be associated with slower startup and increased memory consumption. The resulting binaries may be bloated due to the inclusion of unnecessary dependencies. Moreover, the coin of Spring Boot modularity has another side: it takes effort to master the intricacies of the complex framework ecosystem. Quarkus Developer: Red Hat Top advantages Top disadvantages Kubernetes-native Harder to solve issues due to smaller community Faster startup Quarkus is a Java framework geared towards Kubernetes deployment. It follows the container-first approach, offering the solutions for efficient Cloud deployment: Native support for GraalVM and OpenJDK Hotspot makes it possible to use either AOT or JIT compilation. AOT compiler uses aggressive dead-code elimination to include only the essential dependencies into the resulting native image. The build-time processing allows the framework to collect necessary metadata for better compilation; Faster startup and lower RSS memory consumption thanks to build-time configuration (as a result of which the executable contains only classes utilized at run time) and minimal use of Reflection and dynamic proxies; Convenient scaling in Kubernetes. Quarkus was initially created for cloud-native microservices and serverless, so it utilizes only the Java libraries most relevant for these purposes. At the same time, it is built on top of Java EE standards and enables the developers to use enterprise APIs. Another strong suit of this framework is the ability to use imperative and reactive development models in one application. Reactive frameworks and extensions used by Quarkus aid the developers in writing and managing asynchronous code for better resource usage. On the other hand, Quarkus community is still growing, and it may be difficult to find help or advice on problems encountered. In addition, Quarkus doesn’t implement the full set of CDI (Contexts and Dependency Injection) features and has some Quarkus-specific APIs and non-standard features, which may lead to migration issues. For instance, CDI portable extensions are not supported, so you have to figure out how to write Quarkus Extensions. Micronaut Developer: Object Computing, Inc. Top advantages Top disadvantages Build-in cloud support Not so many options for caching, few build-in endpoints for monitoring Build-time compilation for optimized memory consumption Not always easy to solve a problem due to gaps in documentation Micronaut is tailored to cloud-native microservices by design, so numerous cloud features are natively integrated into the framework, including: Several service discovery systems (Eureka, Consul, and ZooKeeper); Distributed tracing and distributed configuration; Kubernetes and major cloud runtimes support; Client-side load balancing; Serverless functions. As far as memory consumption and startup are concerned, Micronaut uses AOT compilation to process the code at build time, which results in a smaller executable with accelerated startup. GraalVM is also supported, which, coupled with a special Micronaut AWS module, makes this framework a good match for AWS Lambda functions. However, Micronaut offers limited options for caching and monitoring. In addition, it might take time to figure out how to work with the framework and deal with issues as the Micronaut community is quite narrow. Dropwizard Developer: Yammer Top advantages Top disadvantages Minimalistic No built in Dependency Injection Excellent metrics functionality No freedom of choice due to limited amount of supported libraries Dropwizard was designed specifically for the development of RESTful web services and microservices. In essence, it is a lightweight toolkit that includes a minimalistic set of tried-and-true Java libraries: Jetty, Jersey, JUnit, Liquibase, etc. As the official documentation states, Dropwizard “straddles the line between a library and a framework,” meaning that it provides essential performant implementation to quickly spin up a production-ready web service. Moreover, Dropwizard integrates a powerful Metrics library that is sometimes integrated into projects utilizing other frameworks, even Spring Boot. Just like Spring Boot, Dropwizard follows the “convention over configuration” approach, but does so even more zealously. As a result, developers don’t have much freedom of choice when choosing the tools for their application. Furthermore, Dropwizard doesn’t support dependency injection, which may be inconvenient for developers who are used to working with this traditional design pattern. Finally, Dropwizard is suitable for REST services only and doesn’t support SOAP. MicroProfile Developer: Eclipse Foundation Top advantages Top disadvantages Set of Jakarta EE/Java EE APIs Can be confusing for novices Frequent updates The MicroProfile project is aimed at optimizing Enterprise Java for microservices. It is not a framework, but rather a specification consisting of Jakarta EE/Java EE APIs that can be implemented by other frameworks, for instance, Quarkus or Open Liberty. With MicroProfile, developers can utilize industry standards to build resilient Java microservices, which are portable across MicroProfile-compliant runtimes. In addition, they can choose an implementation that suits their business needs, and then migrate to a different one without any issues. MicroProfile developers strive to accelerate the evolution of Enterprise Java by integrating new functionality and updating existing APIs promptly. Therefore, MicroProfile enjoys a short release cycle to keep up with the innovations. At the same time, working with Enterprise Java API may be quite challenging for a novice; plus one must understand the specifics of a selected MicroProfile implementation. Vert.x Developer: Eclipse Foundation Top advantages Top disadvantages Comprehensive toolkit for reactive programming Only for reactive programming Lightweight and embeddable Vert.x is a polyglot development kit for building reactive microservices. Reactive (asynchronous) programming can be complicated, so Vert.x provides a comprehensive technology stack that facilitates writing and maintenance of asynchronous code. As it is not a framework, but a toolkit, it is very minimalistic and embeddable. Developers can integrate it into their project without changing the code structure. Other frameworks use reactive powers of Vert.x: for instance, Quarkus applies it to back its reactive programming model. Most Vert.x APIs don’t block the calling thread, so the application can handle extensive volumes of concurrency without devouring the resources. Given the asynchronous model of Vert.x, it is not suitable for any other type of application. Moreover, it takes a high level of expertise to write good asynchronous code even with Vert.x help, and badly implemented reactivity may even lead to performance deterioration. Besides, Java recently received its own highly efficient concurrency models in the form of virtual thread, which takes the burden of asynchronous programming off the developers’ shoulders. Choosing a framework is only the first step towards the Cloud Building cloud-native microservices is a complex task. Imagine you are actually sending your services into the clouds and beyond, where they should be able to efficiently scale to the size of a constellation. Framework is a toolkit for app development. What else do you need? A launching platform — a reliable Java runtime. Oracle Java is the first option that comes to mind, but considering the recent licensing and pricing changes by Oracle, it would be more cost-efficient to select an OpenJDK distribution; A spaceship — a perfect container for the application. It must be secure, fast, and small. The key component of a microcontainer is a Linux distribution. An overview of the most popular Linux distros for Server and Cloud can be found here. We recommend you opt for Liberica JDK Lite optimized for Cloud, which matches perfectly with a lightweight Linux created specifically for the Cloud — Alpaquita Linux. Alpaquita is based on Alpine, but comes with several enhancements: Two variants with optimized musl and glibc; Three additional malloc implementation for various workloads; Free-to-use Alpaquita Stream (rolling release) and enterprise Alpaquita LTS. A detailed description of Alpaquita Linux is not within the scope of this article. Click the button below to download a white paper on Alpaquita OS where we explore technical characteristics and performance of this distro in-depth. Download white paper on Alpaquita Linux A container with Alpaquita and Liberica Lite is 72.54 MB. What is more, you can use JRE for running the apps — such a container takes up only 41.92 MB; it is the smallest container on the market! Regardless of the framework you choose, BellSoft offers solutions compatible with any of them, so you can build a performant, resilient Cloud infrastructure and reduce TCO of Java apps by min. 20%! - [Master Spring Boot 3 with GraalVM Native Image](https://bell-sw.com/blog/master-spring-boot-3-with-graalvm-native-image/): Spring Boot 3 is riding the wave in the Java world: a few months have passed since the release, and the community has already started migrating to the new version. The usage of parent pom 3.0.2 is approaching 500 on Maven Central! An exciting new feature of Spring Boot is the baked-in support from GraalVM Native Image. We have been waiting for this moment for years. The time to migrate our projects to Native Image is now! But one cannot simply transfer the existing workloads to Native Image because the technology is incompatible with some Java features. So this article covers the intricacies associated with Spring Boot Native Image development. Table of Contents A paradigm shift in Java development Native Image specifics Finalization Initialization Native Image limitations Compatibility with legacy libraries Development and debugging intricacies Performance profile Garbage Collection in Native Image Conclusion A paradigm shift in Java development For many years, dynamism was one of the essential Java features. Developer tools written in Java, such as IntelliJ IDEA and Eclipse, are built upon the presumption that “everything is a plugin,” so we can load as many new plugins as we like without restarting the development environment. Spring is also an excellent example of a dynamic environment, thanks to such features as AOP. Years have passed. We discovered that dynamic code loading is not only convenient but also resource-expensive. Waiting 20 minutes for a web application to start is not fun. We had to think of ways to accelerate startup and reduce excessive memory consumption. As a result, developers began abstaining from excessive dynamism and statically precompiling all necessary resources. Then, GraalVM Native Image appeared. The technology turns a JVM-based application into a compiled binary, which sometimes doesn’t require a JDK to run. The resulting native binary starts up incredibly fast. But Native Image works under the “closed-world assumption,” i.e., all utilized classes must be known during the compilation. So the migration to Native Image is not about changing certain lines of code. It is about shifting the development approach. Your task is to make dynamic resources known to the Native Image at the compilation stage. Native Image specifics Finalization Developing a Spring application is not the same as writing a bare Java app. We should keep that in mind when working with Native Image. To bring Spring and Native Image together, you must dig into some Java peculiarities. For example, let’s take a simple case of class finalization. At the beginning of Java evolution, we could write some housekeeping code in finalize(), set the System.runFinalizersOnExit(true) flag, and wait for the program to exit. public class ShutdownHookedApp { public static void main( String[] args ) { System.runFinalizersOnExit(true); } protected void finalize() throws Throwable { System.out.println( "Goodbye World!" ); } } You will be surprised if you expect a “Goodbye World!” output because this code won’t run with the existing Java versions due to garbage collection specifics. With Java versions 8-10, the app will do nothing, but with Java 11, it will throw an exception with a message that this feature was deprecated: ➜ shutdown_hook_jar java -jar ./shutdown-hook.jar Exception in thread "main" java.lang.NoSuchMethodError: 'void java.lang.System.runFinalizersOnExit(boolean)' at ShutdownHookedApp.main(ShutdownHookedApp.java:9) Why was this feature removed? Finalizers work in some situations and fail in others. Developers can’t rely on a feature with such unpredictable behavior. Native Image documentation states that finalizers don’t work and must be substituted with weak references, reference queues, or something else, depending on the situation. For the Java platform, it is a good development trend to make unpredictable behavior a thing of the past. If you want to guarantee the String output upon exit, use Runtime.getRuntime().addShutdownHook(): public class ShutdownHookedApp { public static void main( String[] args ) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Goodbye World!"); })); } } Spring developers have additional tools. You can use the @PreDestroy annotation and close the context manually with ConfigurableApplicationContext.close() or write something similar to shutdown hook registration. You can do this instead of a finalizer: @Component public class WorldComponent { @PreDestroy public void bye() { System.out.println("Goodbye World!"); } } Or you can use this instead of Shutdown Hook: @SpringBootApplication public class PredestroyApplication { public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(PredestroyApplication.class, args); int exitCode = SpringApplication.exit(ctx, new ExitCodeGenerator() { @Override public int getExitCode() { System.out.println("Goodbye World!"); return 0; } }); System.exit(exitCode); } } Now, let’s collect all these methods in one code snippet: @SpringBootApplication public class PredestroyApplication { public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Goodbye World! (shutdown-hook)"); })); ConfigurableApplicationContext ctx = SpringApplication.run(PredestroyApplication.class, args); int exitCode = SpringApplication.exit(ctx, new ExitCodeGenerator() { @Override public int getExitCode() { System.out.println("Goodbye World! (context-exit)"); return 0; } }); System.exit(exitCode); } @PreDestroy public void bye() { System.out.println("Goodbye World! (pre-destroy)"); } @Override protected void finalize() throws Throwable { System.out.println( "Goodbye World! (finalizer)" ); } } Let’s run the program and see the order of our “finalizers”: Goodbye World! (context-exit) Goodbye World! (pre-destroy) Goodbye World! (shutdown-hook) This code will function when compiling a Spring app into a native image. Initialization Let’s set finalization aside and look into initialization for a change. Spring provides several field initialization methods: you can assign a value directly with the @Value annotation or define properties with @Autowired or @PostConstruct. GraalVM adds another interesting technique, which enables you to write data at the binary compilation stage. Classes that you want to initialize this way are marked with --initialize-at-build-time=my.class when building Native Image. The option works for the whole class, not just separate fields. It is convenient and sometimes even required (if you use Netty, for example). Let’s build a new Spring application with Spring Initializr. The only dependency we need to specify is GraalVM Native Support. You also need a Native Image Build Tool to generate native executables. BellSoft develops Liberica Native Image Kit, a GraalVM-based utility recommended by Spring. Download Liberica NIK for your platform here. Select NIK 22 (JDK 17), Full version. Put the compiler to $PATH with GRAALVM_HOME=/home/user/opt/bellsoft-liberica export PATH=$GRAALVM_HOME/bin:$PATH Check that Liberica NIK is installed: java -version openjdk version "17.0.5" 2022-10-18 LTS OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS) OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode, sharing) native-image --version GraalVM 22.3.0 Java 17 CE (Java Version 17.0.5+8-LTS) Back to Spring Boot. Our application will do the following logic: we initialize a PropsComponent bean and ask it for a “key”: @SpringBootApplication public class BurningApplication { @Autowired PropsComponent props; public static void main(String[] args) { SpringApplication.run(BurningApplication.class, args); } @PostConstruct public void displayProperty() { System.out.println(props.getProps().get("key")); } } Component properties will be loaded in a static class initializer. @Component public class PropsComponent { private static final String NAME = "my.properties"; private static final Properties props; public static final String CONFIG_FILE = "/tmp/my.props"; static { Properties fallback = new Properties(); fallback.put("key", "default"); props = new Properties(fallback); try (InputStream is = new FileInputStream(CONFIG_FILE)) { props.load(is); } catch (IOException ex) { throw new UncheckedIOException("Failed to load resource", ex); } } public Properties getProps() { return props; } } Create a /tmp/my.props text file and populate it with data: key=apple If we build the standard Java app, we get different outputs by changing the contents of the my.props file. But we can change the rules of the game. Let’s write the following Native Image configuration in our pom.xml: native org.graalvm.buildtools native-maven-plugin build-native compile-no-fork package --initialize-at-build-time=org.graalvm.community.examples.burning.PropsComponent Pay attention to the initialize-at-build-time key. Now, build the app with mvn clean package -Pnative. The resulting file is in the target directory. No matter how many times we change the /tmp/my.properties file, the output will be the same (the one we wrote at compilation). On the one hand, it is an excellent tool that increases application portability if you use the properties file for code organization and not for dynamic String loading. On the other hand, it may lead to misuse and incorrect understanding of the code. For example, a DevOps may glance at the code, see the my.properties file, and then spend the whole day trying to understand why the file doesn’t pick his or her settings. This is a fundamental concept of Native Image — the separation of data between compilation and run time. If you build app configuration based on environment variables, you should evaluate which keys will be initialized at a specific moment. For convenience’s sake, it is possible to use different prefixes, like “S_” for static compilation and “D_” for dynamic one. Native Image limitations Some functions work differently or don’t work with GraalVM at all: Reflection Proxies Method Handles Serialization JNI Resources One approach is to accept that they are not supported and rewrite the app accordingly. Another way is to understand what we know at compile time and put this data into config files. Below is the example for Reflection: [ { "name": "HelloWorld", "allDeclaredFields": true } ] To avoid manual configuration, run the app with the standard JVM and the java -agentlib:native-image-agent=config-output-dir=./config flag. While you are using the application, all resources you are utilizing will be written into the config directory. The agent usually generates a whole bunch of files associated with the features mentioned above: jni-config.json predefined-classed-config.json proxy-config.json reflect-config.json resource-config.json serialization-config.json After that, state these files in pom.xml Spring configuration: org.graalvm.buildtools native-maven-plugin -H:ReflectionConfigurationFiles=reflect-config.json If you adjust the Reflection as shown above and then try to access something you didn’t define, the program won’t exit with an error like “Aborting stand-alone image build due to reflection use without configuration.” Instead, it will continue running, and Reflection calls will provide an empty output. It means that you must consider all Reflection calls when writing the tests. Compatibility with legacy libraries The Java ecosystem has a competitive edge over C/C++. With Java, you can add a couple of lines to the pom.xml, and Maven will load a ready-to-use library. In contrast, a C++ developer must put libraries together manually for different platforms. Classical Java enables you to use third-party libraries as ready-to-go boxes without knowing how they were developed. The situation differs with Native Image. Because Native Image uses global code analysis, it compiles the libraries with the application code. Third-party libraries are compiled every time anew on your computer. If there is a compilation error, you will have to solve the issues related to the incompatible library. If you develop an innovative solution, these GraalVM incompatibilities are a great way to find issues in your code or discover new ways of developing your projects. If you write hardcore fintech code, determine whether the third-party library supports GraalVM Native Image. But let’s go back to Spring: what about Native image support there? The Spring team has done outstanding work with integrating Native Image technology into the ecosystem. Just a year ago, Spring didn’t support the technology. Then the Spring Native project was born, and now, Spring Boot has baked-in support for Native Image. The team continues building on the momentum, and many Spring libraries and modules are already compatible with Native Image. Still, we recommend running the libraries with your code using a prototype to make sure that everything works correctly. Development and debugging intricacies Native Image compilation takes time (min. 90 seconds), so it would be more practical to write and debug the code using standard JVM and turn to Native Image only when you have some coherent results. But you should always test the resulting binary separately, even if you double-checked the JAR. Why? An application compiled with Native Image can behave differently than the “classic” JVM version. For example, you have an inexplicit Reflection somewhere in the code. It fails without an error or message, and the code gives a different result. To accelerate the testing process, set the CI server in such a way that it collects all commits through Native Image. You can also save testers' time by providing them with Native Image binaries only, without the JARs. In addition, DevOps engineers should write a console script that can be easily started (ondemand ./project). It builds the project, compiles it with Native Image, packs it into a Docker image, and deploys it to a new virtual machine on Amazon. Fortunately, Spring Boot performs the whole build process with a single command: mvn clean package -Pnative. But virtual machine deployment remains your task. Performance profile Legends and mysteries surround Native Image advantages. They claim it will make apps smaller and faster. But what does it mean, “smaller” and “faster”? Two decades ago, before cloud computing and microservices, developers wrote monolithic applications only (they still prevail in some industries such as gaming). The key performance indicators for monolithic applications are increased raw peak performance and minimal latency. The Just-in-time (JIT) compiler built into the OpenJDK is quite good at dealing with these tasks. But the performance increases only after Tier4CompileThreshold invokes the C2 JIT-compiler, which takes time. It is not optimal for cloud-native microservices. Key performance indicators are different in the cloud: Microservices have to restart rapidly for efficient scaling; Containers must consume less resources so as not to inflate cloud bills; The build process must be simple simple to make the DevOps processes easier; Packages must be small so that developers can rapidly solve issues and move apps between the Kubernetes nodes. The JIT compiler is not suitable for these purposes because of long warm-up time and excessive overhead. GraalVM uses the AOT (ahead-of-time) compilation, which significantly reduces startup time. As for the memory consumption, the resulting native executable is not always smaller than Uber JAR or Fat JAR. So developers should build the project or its part with Native Image and verify whether it is worth the trouble. What can we do to reduce resource consumption? The easiest solution is to decrease the size of base images. It is fast and cheap as there’s no need to rewrite the app or adjust development processes. For instance, a base Alpaquita Stream image with musl is 3.3 MB (compressed), and a glibc-based image is 8.32 MB. Enterprises can consider Alpaquita Cloud Native Platform with Alpaquita LTS, Liberica JDK Lite optimized for Cloud, and Liberica Native Image Kit. There is one more thing to consider when selecting the compiler, namely the load patterns. The AOT compilation is best suitable for a “flat” load profile when application parts are loaded understandably. JIT is optimal for applications with sudden load peaks because JIT can define and optimize such loads. The choice depends on your app. Take a look at your Spring Boot app. Find out which microservices return web pages, which work with a database, and which perform complex analytics. We can safely assume that the load profile of web services will be flatter than that of business analytics, so they potentially can be migrated to Native Image. Garbage Collection in Native Image Native Image is a relatively new project, so it doesn’t utilize the variety of garbage collectors compared to OpenJDK. GraalVM Community Edition (and Liberica NIK) currently use only simple Serial GC with generations (generational scavenger). Oracle’s GraalVM Enterprise also has a G1 GC. Fortunately, BellSoft engineers are currently working on adding Parallel GC to Liberica NIK and GraalVM CE. This GC is better than serial GC and even more optimized for throughput than G1 GC. The first thing to note is that Native Image uses more memory than stated in the Xmx parameter. A standard JVM-based app does that too, but for different reasons. In the case of Native Image, the root cause resides in garbage collection specifics. GC uses additional memory when performing its tasks. If you run your application in a container and set the precise amount of memory in Xmx, it will probably go down when loads increase. Therefore, you should allocate more memory. Use a trial-and-error approach to find out how much. Furthermore, if you write a tiny program, it doesn’t mean it will automatically use less memory. Like with JVM, we have Xmx (maximum heap size in bytes) and Xmn (young generation size) parameters. If you don’t state them, the app may devour all available memory within the limit. You can alleviate the situation with the -R:MaxHeapSize parameter that sets the default heap size at build time. But thanks to these Native Image specifics, we can now conveniently write console applications with Spring Boot. Imagine you want to create a console client for your web service. The first thought that comes to mind is to develop a Spring Boot app and reuse the whole Java code, including classes for API. But such an application would take several seconds to start without a chance for acceleration with JIT because there’s too little runnable code in console apps to trigger JIT. And every application start would consume a lot of RAM. Now you can compile the app with Native Image, set -R:MaxHeapSize, and get a good result, no worse than with standard Linux console commands. For illustrative purposes, I wrote a console jls utility with the same function as ls, i.e., listing the files. The algorithm is borrowed from StackOverflow. @SpringBootApplication public class JLSApplication { public static void main(String[] args) { SpringApplication.run(JLSApplication.class, args); walkin(new File(args[0])); } public static void walkin(File dir) { File listFile[] = dir.listFiles(); if (listFile != null) { for (int i=0; i Define the max. heap size in Maven settings: -R:MaxHeapSize=2m According to the time utility, the execution time for the /tmp directory is about 0.02 s, which is within the time margin of error. And that time includes the start of the algorithm plus the whole Spring Boot app. The result is quite impressive compared to a JAR file. Conclusion Finally, we can compile our Spring Boot projects with Native Image! This powerful utility makes it possible to perform tasks previously unattainable for Java developers. We will continue experimenting with the capabilities of Native Image within the Spring ecosystem, so subscribe to our newsletter and don’t miss further guides and tips on integrating the technology into your Spring Boot project. - [How to dockerize a Spring Boot app with the smallest base image](https://bell-sw.com/blog/how-to-dockerize-a-spring-boot-app-with-the-smallest-base-image/): In one of our previous articles, we discussed buildpacks and how they accelerate development. In this article, we will show two approaches to dockerizing a Spring Boot application: one uses a Dockerfile, and another leverages a buildpack. By the way, another way to optimize Spring Boot footprint in the cloud is to use Java with CRaC support: Alpaquita Containers with CRaC help to create 10% smaller images! Table of Contents Containerize Spring Boot with Dockerfile Build a Spring Boot app Write a Dockerfile Build a Docker container image Run the Spring Boot Docker image Containerize Spring Boot with Paketo buildpack Java container size comparison Conclusion Prerequisites: Docker Your favorite IDE Containerize Spring Boot with Dockerfile Build a Spring Boot app First, let us write a basic Spring Boot application to have something to work with. Go to Spring Initializr. Select your preferred build system (Maven or Gradle), Java, JAR, Java version 21. Then, select the Spring Web dependency. Download the project and unzip it. Open the project in your favorite IDE and create a HomeController class configured in the following way: import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HomeController { @GetMapping public String sayHello() { return "Hello Docker!"; } } Write a Dockerfile We will use a Liberica Runtime Container, which includes a lightweight musl-based Alpaquita Linux and Liberica Lite optimized for Cloud deployment. Liberica JDK Lite is a flavor of Liberica JDK, a Java runtime recommended by Spring. You will need a following Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder WORKDIR /app ADD demo /app/demo RUN cd demo && ./mvnw package FROM bellsoft/liberica-runtime-container:jre-21-musl WORKDIR /app EXPOSE 8080 CMD ["java", "-jar", "/app/demo.jar"] COPY --from=builder /app/demo/target/*.jar /app/demo.jar Here, we build a project inside a Docker image based on Liberica JDK and then copy it into a fresh base image with Liberica JRE, where we can run the application. You need the EXPOSE 8080 line if you have a web application. This Dockerfile enables us to create and run an executable JAR of our Spring Boot app. But you can go even further and use a layered JAR. In this case, the application classes and dependencies are stored in different layers. The layers that are more frequently updated are placed on the top, and other layers can be pulled from the cache, making image updates faster. To build a layered JAR, you need to use the -Djarmode=tools property. When running the resulting image, you will need a special org.springframework.boot.loader.launch.JarLauncher class that knows how to work with layered JARs. The resulting Dockerfile is as follows: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder WORKDIR /app ADD demo /app/demo RUN cd demo && ./mvnw package FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl as optimizer WORKDIR /app COPY --from=builder /app/spring-petclinic-main/target/*.jar petclinic.jar RUN java -Djarmode=tools -jar petclinic.jar extract --layers --launcher FROM bellsoft/liberica-runtime-container:jre-21-stream-musl ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] EXPOSE 8080 COPY --from=optimizer /home/app/dependencies/ ./ COPY --from=optimizer /home/app/spring-boot-loader/ ./ COPY --from=optimizer /home/app/snapshot-dependencies/ ./ COPY --from=optimizer /home/app/application/ ./ Build a Docker container image Run docker build . -t demo-boot-docker Check the image size: docker images REPOSITORY TAG IMAGE ID CREATED SIZE demo-boot-docker latest e67abffec5d3 9 seconds ago 159MB The resulting image based on JRE and a lightweight Linux distro will help you reduce memory footprint in the cloud. But what if reducing the startup time of your Java application in a container is also critical? In this case, you can take advantage of Application Class Data Sharing (AppCDS). There’s no need to adjust the code of your app, only a couple of lines in your Dockerfile. For more details, refer to our guide Using CDS with Spring Boot. Run the Spring Boot Docker image To run the Docker image from the command line, use the following command: docker run --rm -p 8080:8080 demo-boot-docker If you visit localhost:8080, you will see our “Hello Docker!” message. Containerize Spring Boot with Paketo buildpack Let’s take the same project we developed in the previous section. Spring Boot supports building Docker images with buildpacks out-of-the-box, using Maven or Gradle plugin. If you don’t need to configure the image additionally, you can simply run one command to containerize your app without writing a Dockerfile. BellSoft Liberica JDK is the default JVM with Paketo buildpacks. It provides a JDK at build time and JRE at runtime, so the resulting container will be smaller. To build an image from the source with Maven, run mvn spring-boot:build-image If you use Gradle, use the following command to build a Docker image: gradle bootBuildImage Check the images with docker images REPOSITORY TAG IMAGE ID CREATED SIZE spring-boot-docker 0.0.1-SNAPSHOT 372afdfa3ea0 1 minute ago 352MB As you can see, the resulting image is two times bigger than the one we built using a Dockerfile, the root cause being the underlying operating system: the official Spring Boot buildpacks use Ubuntu. But we can still benefit from buildpacks and get a smaller resulting image if we take BellSoft’s buildpacks based on Alpaquita. To do that, you need to add the following configuration to the Maven plugin in pom.xml: org.springframework.boot spring-boot-maven-plugin demo-bellsoft-buildpack bellsoft/buildpacks.builder:musl If you use Gradle, configure the plugin in the following way: plugins { id 'java' id 'org.springframework.boot' version '3.0.6' id 'io.spring.dependency-management' version '1.1.0' } dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' } bootBuildImage { imageName = "demo-bellsoft-buildpack" builder = "bellsoft/buildpacks.builder:musl" } The command for building an image doesn’t change. For Maven: mvn spring-boot:build-image For Gradle: gradle bootBuildImage Check the images: docker images REPOSITORY TAG IMAGE ID CREATED SIZE demo-bellsoft-buildpack latest 0f5f091304d8 1 minute ago 140MB The resulting image is just as small as the one we build with a Dockerfile! Java container size comparison We now have three containers with the same Java application, and the size difference is striking. Our containers based on Alpaquita and Liberica JRE Lite are 159MB and 140MB, which is more than twice as small as the container we built with Liberica JRE and Ubuntu via a buildpack. Alpaquita + Liberica Lite Liberica JRE + Ubuntu via the Paketo buildpack Liberica JRE + Alpaquita via the Paketo buildpack 159MB 352MB 140MB Java container size comparison Conclusion As you can see, switching to a lightweight base OS image makes a drastic difference regarding container size. But migration to Alpaquita gives you additional benefits: Two libc implementations available, optimized musl and glibc; Enhanced performance compared to other popular Linux distributions; The only Linux optimized for Java development; LTS versions as part of commercial support; 100% compatibility with Liberica JDK and Liberica NIK, products used and recommended by the Spring team. Want to know more about Alpaquita? Head to Alpaquita Documentation for install guides, feature overview, and tuning recommendations. - [How to containerize native images](https://bell-sw.com/blog/how-to-containerize-native-images/): Native Image technology is gaining traction among developers whose primary goal is to accelerate startup time of applications. In this article, we will learn how to containerize native images for further deployment in the Cloud. We will use Liberica Native Image Kit (NIK) as a native-image compiler and Alpaquita Stream as a base image. Create an application Create a folder for your demo project. Then go to the project folder and build a simple Java application in the console: cat >./Demo.java < Pull a Docker image Liberica NIK is a GraalVM-based utility for native image generation. It is used as the default native-image compiler with Cloud Native Buildpacks and recommended by the Spring team as a Native Build tool. Liberica NIK is based on the latest patch versions of Liberica JDK (11 or 17) and GraalVM (21 or 22). BellSoft provides a variety of images with Liberica NIK hosted on Docker Hub. For instance, if you want Liberica NIK 21 for Java 11 and musl libc, pull the following image: $ docker container run --rm -it bellsoft/liberica-native-image-kit-container:jdk-11-nik-21.3.3-stream-musl You can skip this step, but pulling an image is recommended if you don’t want to do that every time you repeat the build process. Write a Dockerfile we are going to build a native image straight in a container, which is useful when the development and deployment architectures are different. It is also helpful if you want to build a musl-based image, which takes up less memory than a glibc-based one. Firstly, We need to write a Dockerfile to generate a Docker image container. Put the following file into the application folder: FROM bellsoft/liberica-native-image-kit-container:jdk-11-nik-21.3.3-stream-musl WORKDIR /home/myapp COPY Demo.java /home/myapp/ RUN javac Demo.java RUN native-image Demo FROM bellsoft/alpaquita-linux-base:stream-musl-230404 WORKDIR /home/myapp COPY --from=0 /home/myapp/demo . CMD [“./demo”] Where we Specify the base image for Native Image generation; Point to the directory where the image will execute inside Docker; Copy the program to the directory; Run the javac compiler to create the bytecode of our app; Run the native-image tool to build a native image; Create another image with Alpaquita Linux base image (the native image doesn’t need a JVM to execute); Specify the executable directory; Copy the app into the new image; Run the program inside the container. Build a native image container We recommend closing the browser, IDE, and other programs consuming a lot of memory before building a container. To generate a native image and containerize it, run docker build . Check that the image was create with the following command: docker images REPOSITORY TAG IMAGE ID CREATED SIZE 2921e3483bb2 21 seconds ago 18.4MB Tag the newly created image: docker tag 2921e3483bb2 nik-example Now you can run the image with docker run -it --rm 2921e3483bb2 Hello from Native Image! Conclusion Native image containerization is as simple as creating Docker container images of standard Java apps. Much trickier is to migrate a Java application to Native Image. We used a simple program that didn’t require any manual configuration. But dynamic Java features (Reflection, JNI, Serialization, etc.) are not supported by GraalVM, so you have to make the native-image tool aware of them. If you are a Spring developer and want to benefit from the build-in support for GraalVM Native Image in Spring Boot, check out our guides on specifics and common issues of Spring Boot Native Image development. - [How to use Alpaquita Linux images in AWS](https://bell-sw.com/blog/how-to-use-alpaquita-linux-images-in-aws/): We are happy to announce that Alpaquita Linux images are now available in AWS Cloud! Alpaquita Linux is an Alpine-based distribution with two libc options (improved musl and glibc) and several performance and security optimizations. Alpaquita cloud images will enable developers to build microcontainers and significantly reduce cloud costs. If you are an experienced AWS user, you can search for "Alpaquita" in the AWS AMI Catalog in the Community AMIs tab. For those of you who are just starting with AWS, we prepared a step-by-step guide to launch instances with the Alpaquita image. Table of Contents Your local guide Launch the instance from AMI Instance Name The source AMI Instance Type Networking Storage Ready, Steady, Go Logging in Conclusion Your local guide BellSoft provides a variety of Alpaquita cloud images to fit various needs. You can use our AWS page to select the AMI (Amazon Machine Image) with the combination of parameters you are interested in. Alpaquita cloud images page The "Image details" link under each image takes you to the AWS web interface. You have to log into the AWS web interface, and after that, you will be at the image page where all the information about the image is provided. The page also has a prominent "Launch instance from AMI" button. Image details page on AWS Launch the instance from AMI Clicking on that "Launch instance from AMI" button will take you to the launch page with many forms. Seeing them for the first time might be a bit overwhelming, but you simply need to provide a few obvious bits of information. Instance Name First of all, give your instance a free-form name: Naming an instance The source AMI The next block doesn't require any action on your part. It's already pre-filled with the AMI you selected in the previous step. Data about the AMI from catalog Instance Type Virtual machines in the cloud come in all shapes and sizes. In the following form, you need to select the cloud shape you want. The default is usually quite enough for a quick introduction. You may read more about instance types in the Amazon documentation. Selecting the instance type Networking You should be able to access your cloud instance, so you must provide it with the required information so that it lets you in. That's the point of the subsequent two forms. Setting a key pair and network You must use an existing SSH key pair or create a new one. This key pair will identify you to the instance. You should also tell Amazon firewall that it must let your SSH into your instance. Luckily, these settings are provided by default, as shown in the screenshot. To start a networking server on your instance, you must configure the relevant firewall rules here. Storage The following form allows you to select the amount of disk space your instance will have. If you only want to experiment with the instance, the default might be enough, but if you are going to install apps and test them, you might need to arrange for more space. Here, we have increased the available disk space from 1GB to 5GB for demonstration purposes. Configuring storage Ready, Steady, Go That's it; you are now ready to launch your instance. Review the summary of your settings and click the "Launch instance" button. Summary of settings Logging in When the instance is launched, you can log in with a SSH client using the default user name "alpaquita" and the SSH key pair you specified when configuring the instance. Conclusion Now you can use Alpaquita on Amazon cloud! If you want to know more about ways Alpaquita will help you increase the performance of your containers and optimize cloud costs, read our comparative performance study of popular Linux distros and overview of Alpaquita features. Or download a white paper with detailed technical characteristics of our lightweight Linux. Get Alpaquita Linux white paper - [Liberica JDK 20 is released](https://bell-sw.com/blog/liberica-jdk-20-is-released/): We are happy to announce that Liberica JDK 20 builds are generally available! The major Java release contains numerous fixes and enhancements: 2,442 fixes overall. BellSoft resolved 11 issues; 7 JEPs with new or improved features. The list of integrated JEPs JEP 429: Scoped Values (Incubator) enables sharing of immutable data within and across threads, and thus prohibits the change of a variable by remote code; JEP 432: Record Patterns (Second Preview) promotes more declarative programming by introducing record patterns to deconstruct record values; JEP 433: Pattern Matching for switch (Fourth Preview) enables pattern matching for switch expressions and statements, facilitating complex data-oriented queries; JEP 434: Foreign Function & Memory API (Second Preview) introduces an API that enables Java programs to interoperate with code and data outside of the Java runtime; JEP 436: Virtual Threads (Second Preview) enhances multithreaded programming in Java by introducing virtual threads that don’t hold on to an OS thread for the whole lifetime of the code, so thousands or even millions of tasks can be performed concurrently; JEP 437: Structured Concurrency (Second Incubator) improves reliability of multithreaded code by enabling developers to create a tree-shaped hierarchy of tasks and confine subtasks within one syntactic block. JEP 438: Vector API (Fifth Incubator) enables developers to express vector computations that reliably compile at runtime to optimal vector instructions on supported CPU architectures. More information on each feature you can find in our dedicated article. Download the new builds now! This is the last major release before the LTS version, so if you are planning to migrate to JDK 21 LTS, use Java 20 to test new features and start planning the migration. Head over to Liberica JDK Download Center to get the fresh builds now! Download Liberica JDK - [How to choose Liberica JDK flavor](https://bell-sw.com/blog/how-to-choose-liberica-jdk-flavor/): Liberica JDK is compatible with the broadest range of system configurations on the market. In addition, we provide three different flavors: Standard, Full, and Lite, as well as JRE and JDK builds so that you can choose a perfect configuration for your project. But what is the difference between all these packages? Read on to find out! Table of Contents Liberica JDK packages: Full, Standard, Lite Liberica JDK Standard Liberica JDK Full Liberica JDK Lite Difference between JDK and JRE Conclusion Liberica JDK packages: Full, Standard, Lite Liberica JDK Standard Liberica JDK Standard is a standard OpenJDK distribution with only minor adjustments to the codebase. This package includes all the components necessary for writing, compiling, debugging, and running Java applications. The libraries are implemented as is (vanilla builds), with a guarantee of no vendor lock-in. Liberica JDK Standard is perfect for enterprises looking for a tried-and-true, classical JDK for desktop and server deployment. Liberica JDK Full Liberica JDK Full is based on Liberica JDK Standard but also includes LibericaFX, an OpenJFX implementation for building desktop and GUI applications. In addition, the package also contains a Minimal VM for the development of applications for embedded systems. Liberica JDK Lite Liberica JDK Lite is a unique JDK optimized for cloud instances with a minimal footprint. It is a full-fledged, Java SE-compliant runtime but much smaller than any standard Java distribution. We didn't remove any components to trim down its size without affecting the performance; instead, we introduced multiple enhancements, such as better compression for modules. As a result, Liberica Lite has a higher compression ratio for modules than a classic JDK, thus reducing static footprint. Furthermore, we integrated several backports from the recent versions into Liberica JDK 11 and 17, including: For Liberica JDK 11: JEP 346: Promptly Return Unused Committed Memory from G1, — enables G1 GC to automatically return unused Java heap memory to the operating system in the case of low application activity, — and its dependent changes; JDK-8203469: Faster safepoints, — increases the average CPU time for JavaThreads between safepoints thus accelerating allocation rate for better performance, — and its dependent changes; LTO (link-time optimization) build. Link-time optimization enables the GCC (the GNU Compiler Collection) to write unoptimized intermediate code to object files, which are then optimized by linker as a single module at link time. LTO support in JDK was added as part of JEP 297: Unified arm32/arm64 Port and is aimed at improving performance and reducing the static footprint of shared libraries; For Liberica JDK 17: String deduplication for ZGC, ParallelGC, and SerialGC, first introduced in JEP 192: String deduplication for G1, and then extended to other garbage collectors in Java 18. If the strings are duplicated, i.e., contain the same bytes in their code and value fields, the GC deletes all but one byte arrays and reassigns references, thus reducing memory footprint; LTO build; Supporting CDS archived heap objects for ParallelGC and SerialGC helps to improve startup because there’s no need to create numerous expensive data structures (e.g., the module graph) from scratch. Note that all Liberica Lite binaries go through rigorous Q&A tests before each release, including performance regression testing, so there’s no difference between Standard and Lite flavors in terms of performance. The graph below demonstrates how optimized Liberica Lite coupled with Alpaquita Linux helps to reduce the footprint of the Petclinic Spring application compared with other popular Linux and OpenJDK distributions. Petclinic footprint comparison Liberica Lite is the build we use to create the smallest Java containers on the market. Take a look at the following numbers (as of March 20, 2023): Docker image size comparison The striking difference on the graph raises a question: why don’t we use a JRE-based container if it is four times smaller than the regular one? Well, it depends on your goals. The section below reveals the distinguishing features of JDK and JRE. Difference between JDK and JRE JDK and JRE are the essential concepts of Java programming. In the hearts of them both is a JVM, Java Virtual Machine. JVM provides a runtime environment to execute the Java code. It makes the key feature of Java programs (WORA or “write once, run anywhere”) a reality by compiling the Java bytecode into machine-specific one. JRE, or Java Runtime Environment, provides a set of libraries and binaries to execute Java applications. It doesn’t contain any development tools such as javac, Java debugger, JShell, etc., so it is an optimal choice if you plan to only run Java apps, for instance, in the cloud. JDK, or Java Development Kit, is the superset of JRE. It contains everything that is in JRE plus multiple development tools such as a Java interpreter (java), a compiler (javac), an archiver (jar), a documentation generator (Javadoc), a debugger (jdb), and others for developing and debugging Java applications. The diagram below shows the critical difference between JVM, JRE, and JDK concepts. JVM vs JRE vs JDK To keep your containers slim, use JDK for development and JRE for execution. The Dockerfile below demonstrates how we can build a project with Liberica JDK in a container and then copy the image into another container with Liberica JRE: FROM bellsoft/liberica-runtime-container:jdk-17-stream-musl as builder WORKDIR /home/myapp ADD docker-image-demo /home/myapp/docker-image-demo RUN cd docker-image-demo && ./mvnw package FROM bellsoft/liberica-runtime-container:jre-17-stream-musl WORKDIR /home/myapp COPY --from=builder /home/myapp/docker-image-demo/target . CMD ["java", "-jar", "docker-image-demo-0.0.1-SNAPSHOT.jar"] The resulting JRE-based container with lightweight Alpaquita Linux will be three times smaller than the one with Oracle JDK and Debian. The size reduction is especially relevant for Spring Boot apps, which sometimes can be associated with significant memory footprint. See our article on dockerizing Spring Boot for a detailed comparison of different containerization methods with numbers. Conclusion To sum up, Liberica JDK offers a variety of configurations, each optimized for a specific purpose, and provides: Extended selection of tools for desktop applications in the Full version; Classical JDK for server/desktop use cases in the Standard version; Optimized lightweight build for cloud deployment in the Lite version. And the fact that we provide support for all versions, including legacy Java 6 & 7 and GraalVM Native Image, makes Liberica JDK a unified runtime for any use case and environment. Want to learn more about Liberica JDK? Download the white paper with a detailed technical overview of the product. Get Liberica JDK white paper - [How to deal with Alpine DNS issues](https://bell-sw.com/blog/how-to-deal-with-alpine-dns-issues/): Alpine Linux is a go-to OS for those striving to reduce the size of their containers. It is secure, minimalistic, performant, and free-to-use. What’s there not to like? All is well until one day your Kubernetes clusters start behaving strangely. A closer look at the logs will tell you that the devil is in the DNS, namely in the way your Alpine-based application deals with it. And this devil is hard to chase away. Read on to know how the DNS problems may arise and what you could do to solve or prevent them. Table of Contents The root cause of Alpine DNS issues How the problem manifests musl specifics Possible solutions Lightweight Alpaquita Linux with musl and glibc The root cause of Alpine DNS issues How the problem manifests The issues with DNS resolution exist in musl-based distributions, one of which is Alpine. In a nutshell, an application running in a Docker container on Kubernetes resolves most DNS queries normally, but in case of larger DNS entries, it throws the UnknownHostException or a similar exception such as / # ping google.com ping: bad address 'google.com' and the host resolution fails. This DNS issue is reproducible in specific cases only. For instance, large DNS requests can be encountered in environments like K8s with many nodes running Ingress. But in the majority of cases, the DNS resolution works fine. The downside is that this issue is unpredictable. Even if your containerized application runs fine on Kubernetes now, one day a large DNS entry comes your way and causes havoc in your clusters. musl specifics musl is a lightweight alternative to the glibc libc. It provides smaller overhead, but unfortunately, comes hand in hand with the above DNS problem. Note that this is not a musl bug, but intended behavior stemming from the functional difference between musl and glibc resolvers. musl supports DNS over UDP (user datagram protocol) only, and doesn’t support DNS over TCP, which is used for packets larger than 512 bytes. It also doesn’t support the feature specified by the RFC standard, which allows to increase the size of the UDP packet above 512 bytes (limit for DNS over UDP) via the Extension Mechanism for DNS (EDNS). This leads to limited support for big DNS packets in musl, so in case of large DNS responses, musl resolver exits. The musl author explains that he didn’t add TCP support for the sake of improved performance. Indeed, UPD enables data transmission without establishing or verifying connection, which results in faster data transfer. TCP, in turn, uses a handshake between client and server leading to higher latency and overhead. There are other peculiarities concerning DNS resolution in musl which may cause additional problems. For instance, musl performs parallel querying of name servers and can’t switch to sequential one like glibc. So if you start the Docker daemon with --dns=172.17.42.1 --dns=10.0.2.15, where the former is the local DNS server and the latter is used for external DNS resolving, there is no guarantee that 172.17.42.1 will be tried first, which will lead to unforeseen failures. To find out more about differences between musl and glibc, refer to a dedicated guide. Possible solutions There are feasible workarounds for the issues mentioned above. Firstly, use fully qualified domain names (FQDN) to avoid resolution failures. An FQDN in DNS records ends with a dot (for instance, “google.com.” is a FQDN, whereas “google.com” is not). Secondly, you can use a local caching DNS server for caching and search path routing. It will reduce the load on the CPU and network and can even speed up name resolution in glibc to overcome a sequential nature of the requests there. Below is the example of setting up a caching server with a flexible tool dnsmasq: apk add dnsmasq cat > /etc/resolv.conf << EOF 127.0.0.1 EOF cat >> /etc/dnsmasq.conf << EOF port=53 listen-address=127.0.0.1 strict-order no-resolv no-poll server=IP_address_1 server=IP_address_2 EOF rc-update add dnsmasq default rc-service dnsmasq start Finally, you can use a specialized DNS library to resolve queries longer than 512 bytes. As far as the problem with large DNS queries is concerned, the Alpine community is aware of it and has been working on fixes for quite a while now. The exciting news is that the fixes are ready! The following set of patches applied from the musl upstream repository adds DNS TCP fallback support and additional network fixes. Patches for musl DNS issues 0009-fix-fallback-when-ipv6-is-disabled-but-resolv.conf-h.patch 0010-dns-fail-if-ipv6-is-disabled-and-resolv.conf-has-onl.patch 0011-res_mkquery-error-out-on-consecutive-final-dots-in-n.patch 0012-dns-treat-names-rejected-by-res_mkquery-as-nonexiste.patch 0013-fix-return-value-of-gethostnbyname-2-_r-on-result-no.patch 0014-remove-impossible-error-case-from-gethostbyname2_r.patch 0015-fix-error-cases-in-gethostbyaddr_r.patch 0016-getaddrinfo-add-EAI_NODATA-error-code-to-distinguish.patch 0017-adapt-res_msend-DNS-query-core-for-working-with-mult.patch 0018-res_send-use-a-temp-buffer-if-caller-s-buffer-is-und.patch 0019-dns-implement-tcp-fallback-in-__res_msend-query-core.patch 0020-getaddrinfo-dns-lookup-use-larger-answer-buffer-to-h.patch 0021-dns-query-core-detect-udp-truncation-at-recv-time.patch 0022-dns-response-handling-ignore-presence-of-wrong-type-.patch 0023-dns-response-handling-don-t-treat-too-many-addresses.patch 0024-clean-up-dns_parse_callback.patch 0025-fix-return-value-of-gethostby-name-2-addr-with-no-re.patch 0026-inet_pton-fix-uninitialized-memory-use-for-IPv4-mapp.patch 0027-dns-prefer-monotonic-clock-for-timeouts.patch 0105-dns-check-length-field-in-tcp-response-message.patch These patches allow the developers to use TCP for DNS-related data exchange. For instance, you can check a large DNS reply by calling getaddrinfo() (54 A records 10.23.0.2..10.23.0.55). Before the patches, we could see the following output: IP 10.0.2.15.60368 > 10.0.2.2.53: 7847+ A? node.bell-sw.org. (34) IP 10.0.2.2.53 > 10.0.2.15.60368: 7847*-| 29/0/0 A 10.23.0.48, A 10.23.0.52, A 10.23.0.24, A 10.23.0.54, A 10.23.0.6, A 10.23.0.17, A 10.23.0.2, A 10.23.0.30, A 10.23.0.22, A 10.23.0.21, A 10.23.0.3, A 10.23.0.9, A 10.23.0.4, A 10.23.0.47, A 10.23.0.16, A 10.23.0.14, A 10.23.0.46, A 10.23.0.42, A 10.23.0.8, A 10.23.0.31, A 10.23.0.5, A 10.23.0.13, A 10.23.0.20, A 10.23.0.10, A 10.23.0.36, A 10.23.0.40, A 10.23.0.12, A 10.23.0.7, A 10.23.0.50 (498) IP 10.0.2.15.60368 > 10.0.2.2.53: 8614+ AAAA? node.bell-sw.org. (34) IP 10.0.2.2.53 > 10.0.2.15.60368: 8614*- 0/1/0 (79) However, with the patches, the result is: IP 10.0.2.15.55127 > 10.0.2.2.53: 2485+ A? node.bell-sw.org. (34) IP 10.0.2.15.55127 > 10.0.2.2.53: 3608+ AAAA? node.bell-sw.org. (34) IP 10.0.2.2.53 > 10.0.2.15.55127: 2485*-| 29/0/0 A 10.23.0.47, A 10.23.0.18, A 10.23.0.43, A 10.23.0.41, A 10.23.0.9, A 10.23.0.48, A 10.23.0.11, A 10.23.0.51, A 10.23.0.8, A 10.23.0.29, A 10.23.0.27, A 10.23.0.15, A 10.23.0.5, A 10.23.0.7, A 10.23.0.19, A 10.23.0.17, A 10.23.0.12, A 10.23.0.49, A 10.23.0.2, A 10.23.0.34, A 10.23.0.14, A 10.23.0.38, A 10.23.0.3, A 10.23.0.45, A 10.23.0.10, A 10.23.0.22, A 10.23.0.55, A 10.23.0.20, A 10.23.0.4 (498) IP 10.0.2.2.53 > 10.0.2.15.55127: 3608*- 0/1/0 (79) IP 10.0.2.15.53870 > 10.0.2.2.53: Flags [S], seq 562297716, win 64240, options [mss 1460,sackOK,TS val 2626445450 ecr 0,nop,wscale 9,exp-tfo cookiereq], length 0 IP 10.0.2.2.53 > 10.0.2.15.53870: Flags [S.], seq 877696001, ack 562297717, win 65535, options [mss 1460], length 0 IP 10.0.2.15.53870 > 10.0.2.2.53: Flags [.], ack 1, win 64240, length 0 IP 10.0.2.15.53870 > 10.0.2.2.53: Flags [P.], seq 1:37, ack 1, win 64240, length 36 2485+ A? node.bell-sw.org. (34) IP 10.0.2.2.53 > 10.0.2.15.53870: Flags [.], ack 37, win 65535, length 0 IP 10.0.2.2.53 > 10.0.2.15.53870: Flags [P.], seq 1:901, ack 37, win 65535, length 900 2485*- 54/0/0 A 10.23.0.50, A 10.23.0.2, A 10.23.0.24, A 10.23.0.3, A 10.23.0.8, A 10.23.0.5, A 10.23.0.32, A 10.23.0.29, A 10.23.0.38, A 10.23.0.23, A 10.23.0.40, A 10.23.0.48, A 10.23.0.7, A 10.23.0.30, A 10.23.0.13, A 10.23.0.14, A 10.23.0.26, A 10.23.0.15, A 10.23.0.6, A 10.23.0.43, A 10.23.0.17, A 10.23.0.9, A 10.23.0.20, A 10.23.0.42, A 10.23.0.10, A 10.23.0.44, A 10.23.0.36, A 10.23.0.22, A 10.23.0.11, A 10.23.0.33, A 10.23.0.4, A 10.23.0.18, A 10.23.0.12, A 10.23.0.19, A 10.23.0.16, A 10.23.0.45, A 10.23.0.35, A 10.23.0.31, A 10.23.0.28, A 10.23.0.53, A 10.23.0.39, A 10.23.0.25, A 10.23.0.27, A 10.23.0.51, A 10.23.0.34, A 10.23.0.21, A 10.23.0.46, A 10.23.0.47, A 10.23.0.49, A 10.23.0.41, A 10.23.0.52, A 10.23.0.37, A 10.23.0.54, A 10.23.0.55 (898) IP 10.0.2.15.53870 > 10.0.2.2.53: Flags [.], ack 901, win 63900, length 0 IP 10.0.2.15.53870 > 10.0.2.2.53: Flags [R.], seq 37, ack 901, win 63900, length 0 These patches are already integrated into Alpine 3.18, so upgrading will eliminate the above issues. However, if you are not planning to upgrade Alpine version soon, or don't want to wait for another long-lasting problem to emerge, consider migration to Alpaquita Linux, a 100 % Alpine-compatible distribution backed by vendor support. Lightweight Alpaquita Linux with musl and glibc The motivation behind Alpaquita was to provide companies with a small Linux distribution that overcomes known Alpine issues, including the ones described above. Therefore, we provide two Alpaquita options: with optimized musl and glibc. The BellSoft engineers took the above patches from the musl upstream repository and integrated them into our musl builds for Alpaquita. Therefore, you can use Alpaquita Linux with optimized musl without the risk of encountering DNS issues. However, if you have been using glibc-based Linux or are still cautious about musl, take a look at our glibc-based Alpaquita variant. It is almost as small as Alpine (8.32 MB), and we also tuned the libc implementation so that it shows even better performance than the default glibc in some cases. Comparison of container images based on various Linux distribution Alpaquita comes with other enhancements as well: Increased kernel performance; Additional security features; Four malloc implementations for various workloads; LTS releases as part of commercial support. Coupled with Liberica Lite, Alpaquita enables you to create microcontainers without sacrificing performance and usability. Have we piqued your interest? Download the white paper on Alpaquita to learn more about our Linux distro: technical characteristics, support options, and comparative performance studies. Or head over to Docker Hub and try free Alpaquita Stream with your application right now! Get Alpaquita Linux White Paper - [How to turn AWT applications into native images](https://bell-sw.com/blog/how-to-turn-awt-applications-into-native-images/): AWT (the Abstract Window Toolkit) is a Java GUI widget toolkit. Although Swing largely superseded AWT, the latter is still used alone or in combination with Swing components. This tutorial will demonstrate how to turn AWT programs into native executables using Liberica Native Image Kit (NIK). Install Liberica NIK Liberica NIK is a GraalVM-based native-image compiler supporting GraalVM versions 21 & 22 for Java 11 & 17. NIK Full version can be used to turn AWT/Swing applications into native images on Linux, Windows, and macOS. For our demo, we will use Liberica NIK 22.3.1 for Java 17. Download the utility for your platform and follow the instructions to complete the installation. Set $PATH to Liberica NIK: PATH=/bin:$PATH Create an AWT app Build a simple AWT application (the code was taken here): import java.awt.*; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; public class Sample extends Frame { public Sample() { Button btn = new Button("Button"); btn.setBounds(50, 50, 50, 50); add(btn); setSize(150, 150); setTitle("Simple AWT window"); setLayout(new FlowLayout()); setVisible(true); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent we) { dispose(); } }); } public static void main(String args[]){ new Sample(); } } Add a maven plugin to build an executable jar: org.apache.maven.plugins maven-assembly-plugin package single Sample jar-with-dependencies Go to the project directory and build a jar file with mvn package Build an AWT native image You need to set java.awt.headless property to false Build a native image: native-image -Djava.awt.headless=false -jar target/AwtDemo-1.0-SNAPSHOT-jar-with-dependencies.jar awtdemo Run the image with ./awtdemo AWT native image If your app doesn’t use any resources, Reflection, JNI, or other features not supported by GraalVM, you don’t need to configure anything. Otherwise, explicitly provide resources or any classes used by reflection or serialization to the native-image tool. For that purpose, run the application with Graal tracing agent to dump the resources and dynamic classes used by the application. After that, run the native-image tool with configuration files that you generated. More examples of turning desktop applications into native images can be found in our articles dedicated to Swing and JavaFX. You can also use Liberica NIK with Spring Boot, Quarkus, and Micronaut. - [Liberica JDK 8u372, 11.0.19, 17.0.7, and 20.0.1 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u372-11-0-19-17-0-7-and-20-0-1-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 8u371, 11.0.18.0.1, and 17.0.6.0.1. CPU releases include patches for Common Vulnerabilities and Exposures (CVE). In addition, we release PSU versions 8u372, 11.0.19, 17.0.7, and 20.0.1 with non-critical fixes and general improvements. The release contains 757 fixes and backports overall. BellSoft participated in eliminating 7 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 7 security issues (CVEs) fixed 39 total security fixes in CPU release: in Liberica 8u371: 14 security fixes + 0 in FX; in Liberica 11.0.18.0.1: 13 security fixes + 0 in FX; in Liberica 17.0.6.0.1: 12 security fixes + 0 in FX. In addition, PSU releases include a total of 718 bugs and backports fixed: in Liberica 8u372: 16 security fixes + 118 additional fixes (+ 21 in FX); in Liberica 11.0.19: 15 security fixes + 203 additional fixes (+ 16 in FX); in Liberica 17.0.7: 14 security fixes + 249 additional fixes (+ 30 in FX); in Liberica 20.0.1: 14 security fixes + 12 additional fixes (+ 10 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-21930 7.4 security-libs javax.net.ssl network high none none unchanged high high none CVE-2023-21954 5.9 hotspot gc network high none none unchanged high none none CVE-2023-21967 5.9 security-libs javax.net.ssl network high none none unchanged none none high CVE-2023-21939 5.3 client-libs javax.swing network low none none unchanged none low none CVE-2023-21938 3.7 core-libs java.lang network high none none unchanged none low none CVE-2023-21937 3.7 core-libs java.net network high none none unchanged none low none CVE-2023-21968 3.7 core-libs java.nio network high none none unchanged none low None Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 20 CVE-2023-21930 • • • • CVE-2023-21954 • • • - CVE-2023-21967 • • • • CVE-2023-21939 • • • • CVE-2023-21938 • • • • CVE-2023-21937 • • • • CVE-2023-21968 • • • • Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Liberica Native Image Kit 22.3.2 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-22-3-2-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 22.3.2 for JDK 11.0.19 and 17.0.7 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Note that Liberica NIK releases are aligned with GraalVM release schedule, which underwent drastic changes in 2023. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds will see the light four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Summary of fixes and enhancements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-21930 7.4 security-libs javax.net.ssl network high none none unchanged high high none CVE-2023-21954 5.9 hotspot gc network high none none unchanged high none none CVE-2023-21967 5.9 security-libs javax.net.ssl network high none none unchanged none none high CVE-2023-21939 5.3 client-libs javax.swing network low none none unchanged none low none CVE-2023-21938 3.7 core-libs java.lang network high none none unchanged none low none CVE-2023-21937 3.7 core-libs java.net network high none none unchanged none low none CVE-2023-21968 3.7 core-libs java.nio network high none none unchanged none low None Conclusion BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [Guide to JVM memory configuration options](https://bell-sw.com/blog/guide-to-jvm-memory-configuration-options/): So you have a goal: to reduce the memory footprint of the application. You roll up your sleeves and get straight to business. Unfortunately, there’s no one-size-fits-all answer to the question “How to reduce memory footprint” or enhance any other KPI. There are best practices to follow and common mistakes to avoid, but in reality, everything boils down to meticulous optimizations with due consideration of all variables. To assist you in this arduous task, we prepared a list of the most important JVM flags related to memory management. Note that in most cases, switching to a smaller base image reduces the memory consumption significantly, so there's no need to tweak the memory configurations. For instance, Alpaquita Linux has a base image size of 3.26 MB (musl) and 8.77 MB (glibc), perfect for Java microcontainers. Footprint (and startup) optimization is also possible by using Java with CRaC: give it a try! Table of Contents Heap size options RAM consumption GC selection and logging Selection Logging GC management How to handle OutOfMemoryError Working with Strings Other useful parameters Conclusion Heap size options JVM parameter Description -Xms Sets the initial heap size -Xmx Sets the maximum heap size -XX:MinHeapFreeRatio Sets the minimum percentage of free space after garbage collection -XX:MaxHeapFreeRatio Sets the maximum percentage of free space after garbage collection -XX:MaxDirectMemorySize Sets the limit for the memory allocated to direct byte buffers In some cases, setting the maximum and minimum Java heap size is enough to optimize JVM memory footprint. The optimal heap size depends on your application, so you should experiment with the values before settling on a final number. Setting min. and max. proportion of heap free after GC helps to avoid unnecessary expansion and shrinking of free space and release the unused memory without affecting the performance significantly. For instance, if you set -XX:MinHeapFreeRatio=40 and -XX:MaxHeapFreeRatio=70, then the generation expands if the free space percentage goes below 40% and contracts if the free space exceeds 70%. Direct byte buffers are used by the JVM to perform native I/O operations. As opposed to non-direct byte buffers stored in the heap, direct ones reside outside the heap and therefore are not affected by heap size parameters or garbage collection. By default, the JVM chooses the size of the direct size buffers automatically based on the available memory, so setting the -XX:MaxDirectMemorySize helps to prevent excessive resource consumption. RAM consumption JVM parameter Description -XX:MaxRAM Sets the max. amount of total memory used by the JVM -XX:MaxRAMFraction Sets the RAM limit for JVM in fractions -XX:MaxRAMPercentage Sets the RAM limit for JVM per cent The JVM flags adjusting the heap size do not affect the total memory consumption by the JVM. To limit the total RAM consumption, use MaxRam flags. The heap size will be adjusted accordingly. For instance, if you have 1 GB of memory, setting -XX:MaxRAMPercentage=50 (or -XX:MaxRAMFraction=2) will make the JVM allocate approx. 500 MB to heap. These arguments are especially useful in the case of containerized applications, where they help to adjust the heap size based on the available container memory. GC selection and logging Selection JVM parameter Description -XX:+UseSerialGC Enables Serial Garbage Collector -XX:+UseParallelGC Enables Parallel Garbage Collector -XX:+UseConcMarkSweepGC Enables Concurrent Mark Sweep Garbage Collector (available up to Java 8 only) -XX:+UseG1GC Enables G1 Garbage Collector -XX:+UseZGC (since Java 15) Enables Z Garbage Collector (available since Java 11) -XX:+UnlockExperimentalVMOptions -XX:+UseZGC (since Java 11 up to 15) -XX:+UseShenandoahGC Enables Shenandoah Garbage Collector (absent in Oracle Java, available in major OpenJDK distributions) The default garbage collection settings are enough for many applications. If you would like to enhance some KPIs (in this case, memory footprint), try switching to another collector, whose defaults will be more beneficial to your app, without delving into the intricacies of GC tuning. Java provides a set of GC implementations, each tailored to specific needs and use cases: Serial GC works in one thread and freezes all app threads while performing collection; Parallel GC also freezes all threads, but works in multiple threads itself; CMS GC doesn’t freeze application threads, but instead, uses a few of them to perform its tasks. This collector was deprecated in Java 9 in favor of a more advanced G1 GC; G1 GC utilizes the Garbage-First approach by dividing the heap in areas and collects the garbage in mostly free areas thus releasing lots of memory; Z GC performs expensive work concurrently with the program and doesn’t freeze the app threads for more than 10 ms; Shenandoah GC performs most of its work concurrently with the program, including the concurrent compaction, so the GC pause times are not directly proportional to the heap size. More detailed information on various collectors can be found in our Guide to Java GC. Logging JVM parameter Description -Xlog:gc*::time Stores the GC logging data at the specified location -XX:PrintGC Enables basic logging in Java 8 -XX:+PrintGCDetails Activates detailed logging in Java 8+ -XX:NumberOfGCLogFiles Sets the limit for the number of GC logs in Java 8 -XX:GCLogFileSize Sets the max. size of a GC log file in Java 8 Before adjusting garbage collector settings, learn to understand its behavior. GC logs are text files that provide exhaustive information about GC work: total GC time, memory reclamation and allocation, etc. Note that GC logging parameters vary between Java 8 and Java 9+: -XX:+PrintGCDetails and -Xlog:gc in Java 9+ substitute -XX:PrintGC in Java 8; Java 8 includes the -XX:+UseGCLogFileRotation parameter that enables the rotation of GC logs. It is used together with the -XX:NumberOfGCLogFiles and -XX:GCLogFileSize flags. However, these functions were deprecated in newer Java versions. A new unified GC logging system is implemented with JEP 271. To learn more about the new logging syntax, run -Xlog:help GC management JVM parameter Description -XX:GCTimeRatio Sets the the limit for GC execution time -XX:AdaptiveSizePolicyWeight Specifies how much previous GC times are taken into consideration when calculating current timing goals -XX:+UseCGroupMemoryLimitForHeap Sets the heap size based on the available container memory -XX:ParallelGCThreads Sets the number of Parallel GC threads -XX:G1HeapRegionSize Sets the size of a G1 region -XX:InitiatingHeapOccupancyPercent Sets the heap occupancy threshold triggering a marking cycle Each GC comes with numerous settings that enable the developers to adjust latency, throughput, or memory. The table above provides memory related settings. It should be noted that by default, -XX:GCTimeRatio is set to 99, which means that the application gets 99% of total execution time, and the collector can run for not more than 1% of the time. The -XX:GCTimeRatio and -XX:AdaptiveSizePolicyWeight parameters are helpful when using -XX:MinHeapFreeRatio and -XX:MaxHeapFreeRatio with Parallel GC. How to handle OutOfMemoryError JVM parameter Description -XX:+HeapDumpOnOutOfMemoryError Dumps heap into a file in the case of OutOfMemoryError -XX:HeapDumpPath Specifies the path for the file with heap data -XX:OnOutOfMemoryError="< cmd args >;< cmd args >" Specifies actions to be performed in the case of OutOfMemoryError OutOfMemoryError leads to the application crash and is hard to troubleshoot. The above parameters provide the developers with a lot of information related to the error, so it is easier to detect memory leaks. Working with Strings JVM parameter Description -XX:+UseStringDeduplication Removes duplicate strings during GC (with G1 GC only) -XX:+UseStringCache Caches commonly allocated strings in the String pool -XX:+UseCompressedStrings Uses a byte[] for Strings that can be represented as pure ASCII -XX:+OptimizeStringConcat Optimizes String concatenation operations when possible java.lang.String is the most commonly used Java class. No wonder that Strings take up a significant part of the application memory. We can release the resources by removing duplicate strings and optimizing the String operations with the above parameters. Other useful parameters JVM parameter Description -XX:LargePageHeapSizeThreshold Uses large pages if max. heap is at least as big as the specified value -XX:LargePageSizeInBytes Sets the large page size for the heap XX:+UseCompressedOops Enables the use of compressed pointers (32-bit instead of 64-bit) for heaps less than 32 GB -XX:+TieredCompilation Disables intermediate compilation tiers -XX:TieredStopAtLevel=1 Uses only the C1 compiler -XX:ThreadStackSize Sets the size of thread stack space The -XX:LargePageHeapSizeThreshold and -XX:LargePageSizeInBytes flags enable the developers to operate with large pages (a technique to reduce the pressure on the processors Translation-Lookaside Buffer caches) and make better use of virtual hardware resources. The -XX:+TieredCompilation and -XX:TieredStopAtLevel=1 can be used with Serial GC to turn off the optimizing compiler and reduce memory footprint in some cases. Use them when memory consumption is the only important KPI. Memory to thread stacks is allocated outside of the heap, so it is not affected by heap size parameters. The -XX:ThreadStackSize flag enables the developers to reduce the size of thread stacks. Conclusion There are a few more JVM options left unmentioned in this article, such as the ones adjusting the size of different heap spaces (permanent generation, young generation, Eden, survivor). The reason is that these parameters require extremely fine tuning without significant overall improvement of memory consumption. In addition, recommendations on tuning the JDK for resource constrained containers can be found in a dedicated guide. The list of all options can be downloaded in PDF format under the button below. Get JVM parameters cheat sheet And remember: sometimes it is much cheaper and faster to migrate to a smaller base OS image than spend hours on twisting the JVM parameters, where one imprudent step may lead to degradation of other performance indicators. Discover Alpaquita Linux — we made it for Java developers seeking excellent performance and minimal footprint. Check out the numbers for base Docker images with and without a JDK: Docker images comparison Have we piqued your interest? Click on the banner below to find out more about Alpaquita. - [Guide to Linux package management](https://bell-sw.com/blog/guide-to-linux-package-management/): A plain sandwich satisfies immediate hunger, but more sophisticated dishes usually require a lengthy list of ingredients. The same goes for application development on Linux — the more advanced your needs, the more libraries you must utilize. And to think that many of these libraries require additional dependencies… Quite a challenge! But don’t worry; we prepared a concise guide to the way libraries are managed in Linux so that you get a good understanding of how to put together a perfect OS for your application! Table of Contents How we manage packages in Linux: key notions A comparison of Linux package managers Linux package management on practice with Alpaquita What packages are available in Alpaquita Working with packages Search for packages Install and remove packages Upgrade or downgrade packages Explore Alpaquita Linux on Docker Hub How we manage packages in Linux: key notions Software for Linux systems is distributed in the form of packages. A package is an archive with software files, configuration files, and a list of required dependencies — additional packages required for the software to run. Note that there are direct and indirect dependencies. Direct dependencies are essential for normal package functioning, whereas indirect ones are optional and could be beneficial for better software performance. When you install, upgrade, or delete a package, you may need to do the same to its dependencies. Linux packages are stored in repositories hosted on a remote server. Usually, Linux providers have their own repositories divided into groups with essential software and auxiliary tools, but the actual classification depends on the vendor. Keeping track of packages, their dependencies, and timely updates is hard. Fortunately, package managers shipped with every Linux distro simplify these tasks. A package manager is a utility that automates the process of obtaining, installing, and otherwise managing packages and their dependencies. For instance, developers can update all software simultaneously or particular packages only. In addition, there’s no need for manual installation of dependencies. A package manager scans the metadata provided with a package and downloads all required software. Most package managers install all dependencies by default. A comparison of Linux package managers There are multiple package managers in the Linux world, but developers can’t choose them because they are pre-built into a Linux distribution. We will summarize five package managers in the most popular Linux server/cloud distributions. Linux distributions File format Command Special features APT Ubuntu Debian .deb apt Optional menu-driven interface YUM RHEL/CentOS 7 Fedora 21 .rpm yum Dependency resolution DNF RHEL/CentOS 8 Fedora 22 .rpm dnf or yum Enhanced YUM with better performance APK Alpine Alpaquita .apk apk Lightweight Zypper openSUSE .rpm zypper Can add repositories specified by URI Linux package management on practice with Alpaquita Alpaquita base image is delightfully minuscule, only 3.32MB (musl) and 8.4MB (glibc). The base image can be utilized for simple deployment scenarios or as a minimal starting foundation for a service or a containerized application. In addition, when developing an application for further dockerization, developers can add any required packages from Linux repositories, thus keeping the final Docker image lean and perfectly tailored to their needs. What packages are available in Alpaquita All Alpaquita packages are organized in two repositories: core and universe. Each of them has two branches, for musl and glibc libc. The core repository contains all main tools and libraries, whereas the universe repository includes extra utilities for various purposes. When writing this article, the repositories contained 3 806 packages altogether, so we do not intend to name them all here. Instead, we will divide the software into several major groups and give a few examples so that the readers better understand what Alpaquita offers. The most commonly utilized packages can be classified as follows: Development Java packages and tools (the packages include Java versions 8, 11, and 17, and JDK and JRE) apache-ant: a java-based build tool async-profiler: low overhead sampling profiler for Java jattach: JVM dynamic attach utility maven: a Java project management and project comprehension tool Python packages and tools (numerous tools are provided in the repository, we give just a couple of examples) py3-attrs: Python classes without boilerplate py3-pytest: Python3 testing library py-gdbm: GNU dbm database support for Python python3: Python programming language GCC compiler, tools, and libraries for C/C++ atf: libraries to write tests in C, C++ and shell g++: GNU C++ standard library and compiler gcc: the GNU Compiler Collection libgccjit: GCC JIT Library libstdc++: GNU C++ standard runtime library swig: a compiler that makes it easy to integrate C and C++ code with scripting languages Administration busybox: modular toolbox of common UNIX utilities coreutils: GNU core utilities shadow: PAM-using login and passwd utilities (usermod, useradd, ...) util-linux: Linux utilities Debugging gdb: the GNU Debugger linux-lts-debug: Alpaquita Linux LTS kernel strace: diagnostic, debugging, and instructional userspace tracer xkbcli: xkb command-line tool with interactive debugger Deployment cloud-init: cloud instance init scripts cloud-utils: utilities for interacting with cloud VM images nodejs: JavaScript runtime built on V8 engine, LTS version py3-pip: tool for installing and managing Python packages skopeo: work with remote images registries (retrieving information, images, signing content) Software for the host system docker: pack, ship, and run any application as a lightweight container nginx: HTTP and reverse proxy server (stable version) podman: simple management tool for pods, containers, and images qemu: a generic machine emulator and virtualizer vte3: Virtual Terminal Emulator library Security apparmor: Linux application security framework (mandatory access control for programs) argon2: password-hashing utility audit: user space tools for kernel auditing cryptsetup: block devices encryption utility cyrus-sasl: Cyrus Simple Authentication Service Layer (SASL) gnutls: the GNU TLS library heimdal: Kerberos 5 and security software krb5: the Kerberos 5 implementation libressl: version of the TLS/crypto stack forked from OpenSSL libsasl: Cyrus Simple Authentication and Security Layer (SASL) library nghttp2: HTTP/2 implementation numactl: simple NUMA policy support oath-toolkit: OATH Toolkit One-time password components openssl: toolkit for general-purpose cryptography and secure communication rhash: (RHash) Recursive Hasher Additional utilities apache2: ​​a high performance Unix-based HTTP server bind: the ISC DNS server dnsmasq: a lightweight DNS, DHCP, RA, TFTP, and PXE server mariadb: a fast SQL database server mysql: dummy package for mysql migration postgresql14: a sophisticated object-relational DBMS, version 14 Working with packages Alpaquita Linux utilizes the APK package manager from the apk-tools package with a few differences from the upstream version: We provide support for alternative packages and cleaner DB handling; Alpaquita package repositories include three latest versions of packages, so you can install the latest version or downgrade a package if necessary; The results of apk search are listed alphabetically for convenience. In addition, it is possible to search for reverse dependencies, i.e., packages that depend on the selected library. In addition, Alpaquita Linux is distributed under EULA (End-user license agreement), meaning that all packages are verified against clean licenses, so there’s no risk of license violation. Most commands are similar to those used in APT and YUM/DNF; the differences are summarized in the table below. APK APT YUM/DNF Install a package apk add apt install yum install Remove a package apk del apt remove yum remove Upgrade a package apk add -u apt install yum upgrade Downgrade a package apk add -d – yum downgrade Install a specified version apk add =$version apt install =$version yum install -$version First thing first, we recommend updating a package index before performing any of the actions listed in the sections below. Run sudo apk update Search for packages The basic apk search command searches through files in repositories and lists all available packages alphabetically. If you want to use wildcards, add the -v flag, for instance: apk search -v 'liberica11-lite*' The -r flag enables you to search for reversed dependencies, for example: apk search -r To retrieve all information about a package, run apk info -a Install and remove packages To install the latest version of a package, run sudo apk add The command accepts several package names separated with a comma. You can also install a specific package version with sudo apk add =$version To remove a package, use the following command: sudo apk del Like sudo apk add, this command accepts several packages. Upgrade or downgrade packages To upgrade a package to the latest version, run apk add -u If you don’t specify the package name, all packages will be upgraded. To downgrade a package, use sudo apk add -d Find out more about working with APK in a dedicated guide. Explore Alpaquita Linux on Docker Hub Apart from an extensive selection of packages, we provide numerous ready images: Alpaquita base image; Liberica runtime container with JDK/JRE; Liberica Native Image Kit container for native image generation and deployment; Alpaquita for C/C++; Alpaquita for Python. We prepared an interactive walkthrough of our Docker hub repositories to avoid getting lost in the labyrinth of image tags. Alternatively, head to Docker Hub, choose an image, and test it with your application. Explore BellSoft’s Docker Hub profile All images with Alpaquita Stream are free to use. However, if your company needs enterprise support, we offer Alpaquita LTS with 24/7 technical service, emergency patches, and regular updates both for Java and Linux. Have you already tested Alpaquita Linux and need a comprehensive technical overview to present to the management? Download the white paper with a thorough description of Alpaquits’s features and performance numbers! Get Alpaquita white paper - [Avoiding AWS Lambda cold starts](https://bell-sw.com/blog/avoiding-aws-lambda-cold-starts/): The summer is just around the corner, but it's freezing in your Lambdas, and the cold starts are the culprit. If you have noticed the detrimental effect of this condition on your business, this article is for you. We will look into the reasons for cold starts, find out why your instances warm up agonizingly slowly and learn how to dodge the issue with a few simple techniques. Table of Contents What causes cold starts in AWS Lambda Programming languages Dependencies Lambda Function chains HTTPS calls Recommendations on minimizing cold starts Keep functions warm Reduce dependencies and packages Develop the app with cold starts in mind Use the Coordinated Restore at Checkpoint (CRaC) API Use AOT-compilation Utilize logging and performance monitoring Conclusion What causes cold starts in AWS Lambda A cold start is an inherent side effect of serverless computing where the machine capacities for code execution are allocated on demand. AWS Lambda, a highly available serverless compute service, manages the code in functions invoked only when the code is called: one request per day or thousands per second. To maintain high availability, AWS Lambda needs a ready set of containers to run the functions at any moment. So the function that is no longer needed is kept warm for a limited time and then shut down to free up a container. To spin up the function, AWS Lambda goes through the initialization process, which includes: The init phase, during which Lambda starts the extensions (monitoring and security tools, etc.), the runtime environment (OS, programming language, libraries), and the function. The invoke phase, during which the function becomes fully serviceable. This stage ends after the runtime and extensions signal that they are done. The shutdown phase is when Lambda shuts down the runtime and extensions. After the invoke phase, Lambda maintains the execution environment in case the function is invoked again quickly. If so, Lambda reuses the execution environment and skips the init phase, going straight to the invoke phase — a warm start. If the function is called after being shut down, the process starts from scratch with the init phase — a cold start. Lifecycle of an AWS Lambda function As a result, companies find themselves in the following situation: No available instances lead to cold starts; The application starts up slowly; All the requests are satisfied, and the functions are shut down; The process commences anew with cold starts. The pattern of a sudden high load can be costly, and in the end, you end up paying much more than you expected due to slow warm up. And simultaneously, while your instances are warming up, the user waits until the server responds. Therefore, long and frequent cold starts lead to increased cloud bills and are highly disruptive to the user experience with your product. Several factors contribute to even longer cold starts. Programming languages The speed of function initialization depends on the language. It has been demonstrated that Python and Node.js load much faster, whereas Java and .NET (strictly speaking, a platform, not a language) take longer to initialize. Although the situation has improved compared to 2018, and the difference is no longer striking, it should still be considered. The main reason for Java and .NET slow startup is Just-in-time (JIT) compilation. The JIT-compiler converts the source bytecode into the machine code at runtime. At the same time, it performs necessary optimizations for better long-term performance, which results in an extended warm-up. Dependencies The more dependencies and packages you use, the more time it takes for the provider to load them. To make matters worse, the process repeats with every function initialization. Lambda Function chains Function chains are an excellent method of splitting up a huge function by assigning related tasks to smaller functions. Each function calls another after completing its job, thus guaranteeing shorter response times to user requests. But if something goes wrong at any stage of the process, it can cause Lambda timeout (the max amount of time a Lambda can run), and the function initialization will start again, leading to even worse latency. HTTPS calls The HTTPS call triggers the SSL/TLS handshake, a communication session between client and server to establish a secure connection. When an HTTPS call happens inside the function, it prolongs the invocation time. Recommendations on minimizing cold starts Unfortunately, we cannot eliminate cold starts, but we can minimize their frequency and duration with the following techniques. The good part is that implementing them will give you an optimal result without paying for additional services such as provisioned concurrency. Keep functions warm Warm Lambda functions are initialized but not fully invoked (“frozen”), and they can be “thawed” when a request comes through. This is achieved by implementing handlers with warming logic that ping the function every few minutes and don’t let it die. For that purpose, you can use third-party tools or CloudWatch Events. Reduce dependencies and packages Reduce the number of dependencies and leave only the direct ones required by your application to run. Another approach is to preload dependencies, thus accelerating startup time. The same goes for OS packages: utilize a minimally sufficient set. You can manually remove unnecessary packages or opt for a minimalistic distribution like Alpaquita Linux. The base image of 3.69MB (musl) or 8.32MB (glibc) is sufficient for simple Lambda functions, but it is possible to pull any other essential packages from Linux repositories. Develop the app with cold starts in mind Developers should consider the response times and timeout risks when developing and preparing an app for deployment in AWS Lambda. For instance, they can reduce the number of static variables or use lazy loading in DB whenever possible. In addition, it is vital to keep functions small and straightforward. Huge functions take a lot of time to load and are associated with significant latency when processing complex requests. Minimalistic single-purpose functions start up fast, complete their task, and release the instance into the pool; the more instances are available, the lower the risks of creating new ones for further requests. Use the Coordinated Restore at Checkpoint (CRaC) API Coordinated Restore at Checkpoint (CRaC) is an OpenJDK Project aimed at reducing the startup and warmup times of Java applications from minutes to milliseconds. Java with CRaC enables developers to take a snapshot of a running application, save it to a file, replicate among instances, and then restore the state of an application from the file. As a result, the application starts almost instantly at peak performance, so you don't have to pay for CPU cycles required for the warmup. BellSoft offers ready-to use Alpaquita Containers with CRaC support so that you don't have to configure Java or the OS to work with this feature. Head to our tutorial on using CRaC with Java containers and give it a try! Use AOT-compilation AOT-compilation is an alternative to the CRaC Project. The Ahead-of-time (AOT) compiler translates the bytecode into the machine code before program execution. It generates a native executable file, which starts almost instantly because searching for hotspots and performing bytecode interpretation is unnecessary. All the optimizations are conducted before the program has started. An AOT compiler eliminates unused code and dependencies, and coupled with the fact that the resulting file doesn’t need a JVM to run, it helps to drastically reduce memory consumption; accelerate startup up 1/10 s; reach peak performance immediately without warm-up; increase the security thanks to a smaller attack surface. AOT compilation is made possible in Java through the GraalVM compiler, which aims to produce highly performant and resource-efficient native images, perfect for cloud-native applications. Indeed, native images with instant startup eliminate the possible damage from cold starts by reducing the function warm-up times. Note that AOT-compiler doesn’t support dynamic features (JNI, Reflection, etc.), so you need to make the compiler aware of them or rebuild the app accordingly. Try out the AOT-compilation right now: BellSoft develops Liberica Native Image Kit (NIK), a GraalVM-based tool for native image generation recommended by Spring. Liberica NIK is always based on the latest GraalVM and Liberica JDK 11 & 17 versions with security patches, bug fixes, and enhancements. Utilize logging and performance monitoring Monitor the behavior of your functions: how often they are invoked, how long the cold starts are, etc. Measure KPIs such as latency and throughput. Understanding the bottlenecks of the app’s performance can help you introduce improvements and reduce cold starts eventually. AWS CloudWatch is an outstanding tool for collecting, visualizing, and analyzing metrics for your Lambda functions. Conclusion If you work with Lambdas or any other serverless computing service, you must deal with cold starts regularly. It’s impossible to eliminate their occurrence, but following the recommendations above will end the Ice Age in your Lambdas: your functions will warm up much faster, and instances will be used more efficiently. Heads up for the Java developers: you don’t have to migrate your application from Java to Python to accelerate function startup. Java may require more space in the cloud and warm up slowly. This is true. But there are numerous ways to alleviate the matter: Use JRE instead of JDK for deployment so that your runtime initializes faster; Consider AOT-compilation for almost instant app startup; Migrate to a smaller base image with minimal packages that must be loaded during initialization. In addition, BellSoft develops a full stack of Java technologies for cloud-native Java applications: Liberica Native Image Kit for native image generation; Alpaquita Linux, a lightweight but highly performant Linux tailor-made for Java. It can be used as a lightweight base image for Lambdas or any other cloud service and CI/CD. We provide ready images with Java, Python, GCC, and Native Image; Liberica Lite, a version of our Liberica JDK optimized for the cloud. All our technologies are free to use. But if you need an enterprise-grade solution, we have the Alpaquita Cloud Native Platform that combines the above technologies plus 24/7 support from a major OpenJDK vendor. Alpaquita Cloud Native Platform is a convenient all-in-one solution for Lambdas, but compatibility with multiple system configurations enables you to develop applications for any purpose (cloud, server, desktop, and embedded), deploy them to any environment, and unify the enterprise Java stack. Click on the button below to learn more about the offer, and may there be spring in your Lambdas! - [How to use Alpaquita Linux images in Microsoft Azure](https://bell-sw.com/blog/how-to-use-alpaquita-linux-images-in-microsoft-azure/): Base OS image plays an essential role in optimizing the size of cloud instances, which, in turn, directly impacts the cloud costs. Microsoft Azure now offers instances with Alpaquita Linux, an Alpine-based Linux distro with numerous optimizations for enterprise development. You can use Alpaquita Stream for free and pay for Azure VMs only, or you can subscribe to commercial support with LTS releases and 24/7 service. This article provides a detailed guide on launching Alpaquita Linux in Azure. If you are an experienced Azure user, search for "Alpaquita" in the Marketplace. You know what to do. Find the Alpaquita image in the Marketplace First of all, go to the Azure portal and click on the “Create a resource” button. Search the Azure Marketplace for Alpaquita and select it in the drop down menu with the search results. You can choose the desired image type in the "Create" menu at the bottom of the card or click on the card to view the description and select the desired image type on that page. We offer two types of images based on the C library type (musl or glibc): musl perf optimized by BellSoft, with equal or superior performance to that of glibc; Enhanced glibc. Create form You are now looking at the Azure VM form. You need to fill in some data, although defaults are fine for the most part. Below we will point out a few key bits that you must provide or might want to change. Basics Tab You must fill in the "Virtual machine name" field. If you are not very familiar with the way Azure works, it might be a bit confusing that this field is not the first field of the form. But ignore the resource group field for now. After that, select the "Size" of your VM, that determines the number of CPUs and the amount of RAM. Resource group An Azure VM comprises several resources: disks, public IP addresses, network firewall rules, etc. Microsoft Azure groups resources into resource groups for easier overview and management. For a new VM, Azure form automatically suggests creating a new resource group named after the created VM. If you wish, you can change the suggested name of the new group, or use an existing group instead. A new resource group will be created. The user and SSH key A user will be created automatically. The default name is azureuser, which is probably just fine, but you can change the name if needed. The user doesn't have a conventional password. To access the VM as this user, you need to identify yourself with an SSH key. Let Azure generate one or use your existing key. Now, click “Next” to proceed to the Disks tab. Disks Tab In this tab, select the storage type to use for the primary OS disk. Moreover, if you have data disks that you want to attach, you can do that in this part of the form. You can also create new unformatted data disks to attach to the new VM here. Networking Tab You might want to check the Networking tab next. For a simple demo, the defaults are just fine, but you can select “Advanced” settings for the NIC security group and edit the firewall rules or use existing ones. You probably also want to select the "Delete NIC when VM is deleted" checkbox for this demo. Monitoring Tab You most likely don't want to change anything in the Monitoring tab, but note that the "boot diagnostics" is enabled here. It will come in handy later on. Advanced Tab Alpaquita supports Microsoft Azure Linux Agent and so you can specify custom data for the newly created VM. The custom data is applied once when a VM is provisioned. For example, you can provide a shell script to run once: Review and Create You are ready to create the VM. Go to the Review+Create tab and click “Create.” It will take some time for your new VM to be deployed. When the deployment process is complete, click “Go to the resource.” Your new VM You will be taken to your new VM. Note its public IP address. Browse the console log (optional) Now you can go to the Boot diagnostics tab in the left side bar (you might need to scroll it down a little bit). Switch to the Serial log tab, where you can see the output from the boot process. You can scroll down and find the host SSH key fingerprint in the output. SSH into your new VM In your terminal, start your SSH client to log into your new VM. If you changed the username, adjust the -l option argument accordingly. The command in this example also tells ssh to accept and ignore the host key, sets up agent forwarding, and creates port-forwarding back to the ssh server on your host so that you can copy files from the cloud VM back to your host even if it's behind a corporate firewall. You can use sudo without a password (your cloud user doesn't have one anyway). You can also see that the custom data shell script was indeed executed. Congratulations, your Alpaquita instance is ready to be used! Conclusion As you can see, Alpaquita instances on Microsoft Azure are just one click away. If you want to know more about ways Alpaquita will help you increase the performance of your containers and optimize cloud costs, read our comparative performance study of popular Linux distros and the overview of Alpaquita features. Or download a white paper with detailed technical characteristics of our lightweight Linux. Get the white paper on Alpaquita - [Java celebrates 28 years of history](https://bell-sw.com/blog/java-celebrates-28-years-of-history/): The Java programming language celebrates its 28th birthday on May 23, 2023 — a perfect age in human terms: still full of energy and ambition but already backed by experience. Java has been among the top three most popular programming languages for many years, according to the Tiobe index, and it continues gaining traction in every sphere of our lives, be it banking applications, social networks, games, or IoT and embedded systems. This year, we decided to mark the occasion by paying tribute to the source of Java’s unquenchable vitality and sustained progress — the Java community worldwide. Nested in the OpenJDK project The secret of Java’s rapid development is that the language doesn’t belong to a single company. In 2006, Sun Microsystems announced that Java would be open-sourced, which set off the OpenJDK project. In 2010, Sun was acquired by Oracle. The company leads many working groups, committees, and projects, makes the biggest contribution to OpenJDK in terms of fixes and patches, and releases two builds with every release: Oracle Java and Oracle OpenJDK. Speaking of releases, as per OpenJDK schedule, Java boasts six releases yearly, two major and four CPU (Critical Patch Update) ones with security patches and bug fixes. In addition, LTS (long-term support) versions traditionally used by enterprises see the light every two years. As promised originally by Sun, the OpenJDK code is freely available to everyone, which means that any individual developer or organization can contribute to the project by Fixing bugs, Patching vulnerabilities, Implementing or deprecating features, Aiding in the development of projects outside of the main branch. If you think that uncontrolled contributions wreak havoc within the project, let us assure you that they are strictly regulated by the internal processes of testing, reviewing, and approving any changes to the codebase. Nevertheless, the fact that any programmer can take part in Java enhancement accelerated the language transformation. Another unique advantage of the OpenJDK project is the promotion of healthy competition. Oracle is not the only company providing Java runtime. Numerous vendors offer their builds based on the raw OpenJDK code. As long as the Java distribution is TCK-verified (Java SE compliant) and all changes go upstream, you can migrate from one vendor to another without issues. Why is it important to have several JDKs to choose from? Because enterprises can pick an offer tailored to their business needs, for instance: Wide range of supported system configurations, Additional features such as OpenJFX or reimplementation of Java Web Start, Support for legacy Java 6&7, Additional utilities, Minuscule Docker containers, Competitive support prices, Prompt 24/7 help from Java engineers, And so on. Moreover, due to the recent changes in Oracle’s pricing policy for Java, companies have an opportunity to choose a more affordable solution and keep their IT costs lean. Backed by market leaders Although Oracle remains the most significant contributor to OpenJDK, numerous large organizations works on making Java more secure, performant, and flexible. They include Companies engaged in Java development for more than 20 years such as BellSoft, SAP, or Azul Systems, Major cloud providers such as Amazon, Multinational corporations such as Microsoft, Google, or IBM, Leading microprocessor manufacturers such as Intel and ARM, Global ICT (information and communications technology) and smart devices providers such as Huawei, Huge e-commerce companies such as Alibaba, And many others. The following chart published in the article dedicated to Java 20 release shows the contributions made by Oracle and other organizations to Java 11 through Java 20: Contributions to JDK 11-20 The communal effort of independent Java developers and organizations helps Java adapt to the rapid changes in the IT landscape. In order not to make unsubstantiated statements, let’s address the most significant Java advancements in recent years: The conquest of the cloud with new features like container-aware memory settings, minuscule containers, and frameworks for cloud-native applications such as Spring Native within Spring, Quarkus, or Micronaut; Explorations of new fields such as IoT, Embedded systems, Edge computing, and many others thanks to Arm and RISC-V ports; Exciting prospects of multilingual programming with GraalVM — a next-generation JVM and JDK written in Java that enables smooth integration of non-JVM code into the project and enhances the app performance thanks to an optimized compiler. What is more, it enables the AOT (Ahead-of-time) compilation of JVM-based programs for an almost instant startup, thus eliminating the notorious JVM warmup; New horizons of multithreaded programming with virtual threads that enable lightweight scalable concurrency similar to Go coroutines; Perks for developers that make coding in Java a pleasant experience. For instance, Records provide compact syntax for declaring classes with immutable data, Record patterns enable declarative and composable form of data processing, Structured concurrency improves the maintenance and observability of multithreaded code, Project Lombok reduces boilerplate code and accelerates the development process. Java is getting more powerful, flexible, and user-friendly with every release thanks to the cooperation of visioners and corporations embracing innovations! Nourished by the thriving global community Java is not simply a programming language but a platform for enthusiasts worldwide to share their knowledge and discuss ideas. All Java developers become part of a friendly community where they can always get help or advice. The pillars of this community are Java User Groups (JUGs) — volunteer organizations aiming to spread Java expertise. They provide a meeting place for developers to share knowledge, resources, ideas, and solutions related to Java and thus help to form an incredibly inclusive community centered around a passion for Java tech. JUGs are typically tied to a particular location, for instance, Atlanta JUG, Chicago JUG, Melbourne Java & JVM JUG, London Java Community, and dozens of others. JUGs play a vital role in Java’s evolution as they Organize monthly meetups and annual conferences for developers to discuss the latest trends, best practices, ad hoc issues, and specialized topics; Participate actively in developing Java libraries and specifications, including Jakarta EE. Every year, there are more than 30 conferences dedicated to Java held around the world, where newcomers and seasoned developers can stay up-to-date with Java trends, do networking, and connect with their peers. BellSoft is proud to continue the tradition of driving innovation forward through collaboration and discussion. Our engineers participate in conferences and JUG meetups on a regular basis, and we also organize JRush, a series of free web conferences for Java developers, where top experts present the newest tools and current trends in Java development. The next episode, scheduled for March 31st, is dedicated to modern Java technologies for Banking and FinTech. Register now to enrich your experience in this area! Register for JRush With more than 60 projects in the making or already integrated into the OpenJDK code, we look forward to numerous exciting improvements and novelties within the Java ecosystem. And all of that is thanks to the engagement of individual developers and organizations. So let’s continue working together to pursue a sustainable future fortified by Java technologies! - [Best Oracle Java Alternatives in 2026: Comparison of OpenJDK Distributions](https://bell-sw.com/blog/oracle-java-alternatives-comparison-of-openjdk-distributions/): The main alternatives to Oracle Java are vendor-supported OpenJDK distributions: Liberica JDK by BellSoft, Azul Zulu, Eclipse Temurin, Amazon Corretto, and Red Hat OpenJDK. Unlike bare OpenJDK, these distributions offer TCK-verified builds, predictable security update cadences, and commercial support options. Spikes of interest in Oracle Java alternatives coincide with recurrent changes to Oracle’s Java license, the latest of which — a new pricing model based on employee headcount — has brought especially significant consequences for large enterprises. If you are tired of unpredictable licensing changes, migrating to a vendor-supported OpenJDK distribution gives you a free, compliant, and commercially supported Java runtime without Oracle's audit risk. We prepared an overview of the eight most popular solutions with a comparative table of prominent features. Download the comparison table (PDF) — all key features across eight distributions in one place. Not All JDK Distributions Are the Same "They're all based on OpenJDK — it doesn't matter which one you use." That's what we keep hearing from developers. It's not quite right. All OpenJDK distributions do come from the same open-source codebase, and TCK verification guarantees that if your application runs on Oracle JDK, it will run on any TCK-verified distribution. The difference is what each vendor chose to do with that code. Some left it essentially as is — a clean baseline that works, but requires you to do your own performance tuning and security hardening. Some adapted it for their own ecosystems, which can make future migration harder. Others went further: adding features, optimising for specific workloads, and building ready-to-use solutions that save developers time and effort. Version and platform support is the most obvious difference. While most distributions cover current LTS releases (Java 8 through 25), only a handful still provide security updates for Java 6 and 7. Supported operating systems and CPU architectures vary significantly from one distribution to another. Container support has become a key differentiator. Most vendors now offer pre-built images for common Linux distributions. Some have gone further and optimised their images specifically for containerised workloads — cutting disk space and resource usage significantly. BellSoft's Liberica Runtime Container on Alpaquita Linux is the most compact at 45.95 MB, saving up to 30% in disk space and RAM compared to standard alternatives. Security is not uniform across distributions. Oracle releases two types of security updates on a quarterly schedule: CPU (Critical Patch Update), which include security-only fixes, and Patch Set Updates (PSU), which include both CPU security fixes and critical non-security bug fixes. Not all vendors ship CPU separately — some release PSU only, meaning every security update also includes bug fixes that may affect application behaviour and require additional testing in production. For organisations in regulated industries or with strict patching SLAs, this distinction matters. Hardened container images are offered as a first-party product by only two vendors in this comparison: BellSoft and Red Hat. All others depend on third-party providers, meaning the runtime and the OS layer are maintained by separate vendors, on separate timelines, under separate security policies. In practice, there is no single vendor accountable for the full stack. Specialised features separate baseline distributions from full-featured ones. Bundles with OpenFX, CRaC support for faster startup, GraalVM for native image compilation, CDS-enabled container images, and performance-optimised JVM variants are each available only from specific vendors. Vendor lock-in is a risk worth naming explicitly. Amazon Corretto and Microsoft Build of OpenJDK offer commercial support only within their respective cloud platforms. SapMachine is built primarily to serve SAP's own ecosystem. Azul Platform Core is a commercial product with its own licensing terms, and it’s not open-source. Distributions tied to a single vendor's ecosystem can complicate future migrations. So what's the best distribution? It depends on what you need. If you want a simple baseline JDK for standard Linux workloads, most distributions will serve you equally well. But if you need support for legacy Java versions, a custom support roadmap, easy compliance with supply chain regulations, hardened container images, or specialised flavors for cloud and container workloads, you need a vendor-supported distribution that covers all your needs out of the box. Comparison of Major OpenJDK Distributions (2026) Liberica JDK (BellSoft) Azul Zulu Red Hat OpenJDK Eclipse Temurin Amazon Corretto Microsoft Build of OpenJDK SapMachine IBM Semeru Runtimes Comparative table of Java runtimes Liberica JDK (BellSoft) License & cost GPLv2 + Classpath Exception Free for commercial use 100% open source Support Commercial SLA available Covers Liberica JDK, Alpaquita Linux, and container images under one agreement. Bug fixes, security patches, and custom builds. Pricing never raised since founding. LTS versions Java 8 Java 11 Java 17 Java 21 Java 25 Java 6 (Extended support) Java 7 (Extended support) All LTS versions are free. JDK and JRE builds for all versions. Features & special flavors OpenFX GraalVM / Liberica NIK Liberica JDK Lite Performance Edition CRaC support Containers 45.95MB on Alpaquita Linux, supporting CDS and CRaC Pre-built also available as JDK or JRE builds for Alpine Linux, Debian, and Rocky Linux on Docker Hub, GHCR, MCR, GCR, and Amazon ECR. Buildpacks Yes Default runtime in the Paketo Spring Boot Buildpack. Hardened Spring Boot Buildpack also available. Hardened images Yes SLA for CVE remediation. Built and maintained entirely in-house by BellSoft. Liberica JDK is an open-source, TCK-verified Java runtime developed by BellSoft, a recognised contributor to the Java ecosystem and a member of the OpenJDK Vulnerability Group, the Java Community Process Executive Committee (JCP EC), and the GraalVM Project Advisory Board. BellSoft engineers contribute fixes and patches to OpenJDK, including those developed for commercial customers, which means there is no vendor lock-in. Improvements flow back into the open-source codebase. Liberica JDK is the only OpenJDK distribution recommended by the Spring team, and the default runtime in Paketo buildpacks for Spring Boot. Liberica Native Image Kit (NIK) is the default GraalVM runtime in Paketo buildpacks. For teams building cloud-native Spring Boot applications, Liberica JDK is the natural, out-of-the-box choice. BellSoft provides enterprise support and security updates across a broad version range: legacy Java versions 6 and 7, all current LTS versions (8, 11, 17, 21, and 25), and GraalVM Native Image technology. LTS versions enjoy an extended support lifecycle of 8.5 years or more for certain versions. Liberica JDK also supports the broadest range of system configurations available from any single vendor — Linux (glibc and musl), Windows, macOS, Alpine, and Alpaquita Linux, across x86 (32- and 64-bit), ARM (32- and 64-bit), PowerPC, RISC-V and SPARC architectures. BellSoft offers dedicated flavours of Liberica JDK tailored to specific workload requirements: Standard — vanilla builds of OpenJDK. Full-featured builds with all necessary libraries provided as is. Compatible with any standard Java SE workload. Full — includes OpenJFX for writing GUI applications. Liberica JDK Lite — optimised for containerised environments, with a minimal footprint and efficient resource usage. Based on Alpaquita Linux, Liberica JDK Lite containers take up only 45.95 MB — the smallest production Java container image available. Performance Edition — delivers up to 10% performance improvement for Java 8 and 11 workloads through low-level JVM optimisations, especially relevant for applications that cannot yet migrate to modern LTS versions. Builds with CRaC support — available for Java 17 and 21, enabling Coordinated Restore at Checkpoint for dramatically faster application startup in autoscaling and serverless environments. Liberica NIK — GraalVM-based ahead-of-time native image compilation, producing standalone native executables with near-instant startup and reduced memory overhead. BellSoft engineers bring deep expertise in optimizing Java performance on ARM processors. They implemented JEP 315 (Improve Aarch64 Intrinsics) into OpenJDK and continue to maintain the ARM32 port, a combination that makes Liberica JDK a strong choice for embedded systems, IoT, and organisations migrating to ARM-based cloud infrastructure such as AWS Graviton. BellSoft provides pre-built container images with Liberica JDK on Docker Hub, GHCR, and the Microsoft Container Registry (MCR), for both x86_64 and AArch64 architectures. The flagship option for container-native workloads is the Liberica Runtime Container on Alpaquita Linux — BellSoft's own musl and glibc based secure container OS, built, maintained, and supported entirely in-house. This small production Java container image can save up to 30% in disk space and RAM usage compared to standard alternatives. Liberica Runtime Container images also include Class Data Sharing (CDS) support for improved startup performance in containerised deployments and CRaC support. Images are also available for Alpine Linux, Debian, and Rocky Linux. For security-conscious teams and regulated industries, BellSoft publishes Hardened Images — zero-CVE container images built and maintained entirely in-house, with SBOM included and image signing enabled. Liberica JDK is free for commercial use, with quarterly CPU (Critical Patch Update) releases aligned with Oracle's Java update schedule. Commercial support is available with flexible plans and without man-in-the-middle. The 24/7 "follow-the-sun" support model operates under a strict SLA, with emergency and off-cycle patches available for critical issues. Pricing is subscription-based per server or desktop; the number of CPUs, processor class, and storage capacity are not factors. Commercial customers also receive patent and non-contamination indemnification. Best for: Spring Boot developers, DevOps and platform engineering teams, enterprises migrating from Oracle JDK, workloads requiring ARM optimisation, and any organisation that values licensing predictability alongside broad platform coverage. Licensing: GPLv2 with Classpath Exception — free for commercial use, no field-of-use restrictions. Eclipse Temurin License & cost GPLv2 + Classpath Exception Free for commercial use 100% open source Support Via 3rd party Community only. No commercial support from the Eclipse Foundation directly. Support subscriptions available from third-party Adoptium Working Group members (Azul, Red Hat, IBM) LTS versions Java 8 Java 11 Java 17 Java 21 Java 25 JDK and JRE builds. Quarterly releases aligned with Oracle CPU schedule. Features & special flavors - Containers 71.81MB on Alpine (default) Pre-built images are also available with Ubuntu, Red Hat UBI9-minimal, and Windows on Docker Hub. Buildpacks Yes Hardened images Via 3rd party No first-party hardened images. Hardening, SBOM generation, and image signing depend on third-party providers. Eclipse Temurin (formerly AdoptOpenJDK) is a free, TCK-verified OpenJDK distribution developed under the governance of the Eclipse Foundation, a member of the JCP Executive Committee. The Adoptium Working Group, which oversees the project, includes strategic and enterprise members such as IBM, Azul, Google, Alibaba Cloud, Huawei, Microsoft, and others. This broad industry backing is Temurin's most distinctive credential. It is the closest thing the Java ecosystem has to a true vendor-neutral, community-governed distribution with significant corporate sponsorship. That distinction comes with an important nuance worth understanding clearly. The Adoptium Working Group provides the platform for JDK distribution and governs the project. It does not directly support the Eclipse Temurin builds in a commercial sense. The Eclipse Foundation itself does not offer commercial support subscriptions. Enterprises that require SLA-backed support must purchase it separately from Adoptium Working Group members such as Azul, Red Hat, or IBM — each under a separate agreement, with separate pricing, and with no guarantee of cross-vendor consistency in patch cadence or escalation process. Free-to-use LTS releases (Java 8, 11, 17, 21, and 25 ) are available across a wide variety of platforms, updated by the Adoptium community on a quarterly cadence aligned with Oracle's CPU schedule. In practice, Temurin releases tend to follow the Oracle CPU schedule by a few days to a week, slightly later than some vendor distributions. JDK and JRE builds are provided for all supported versions. Adoptium has committed to building binaries for LTS releases as long as the corresponding upstream OpenJDK source is actively maintained. For teams evaluating Temurin alongside other distributions, the feature set is deliberately minimal. It is a strength for those who want an uncomplicated, standards-compliant baseline, and a limitation for those who need performance-optimised builds, CRaC checkpoint support, native image compilation, or OpenJFX. None of these are available in Temurin. On the container side, Adoptium provides images across Alpine, Ubuntu, Red Hat UBI9-minimal, and Windows. All images are published exclusively on Docker Hub. There are no CDS-enabled images. Hardened container images, their SBOM generation, and image signing are not available directly from the Eclipse Foundation. It depends on third parties, which introduces an indirect dependency on external vendors' update cadences and provenance practices. The Paketo Buildpack for Java is supported. For teams that need commercial support, the fragmented support landscape is the most significant practical limitation. Depending on which Adoptium Working Group member a team buys support from, the terms, pricing, response times, and coverage scope will differ. Best for: Teams prioritising vendor-neutral governance and open-source community alignment over feature breadth. Licensing: GPLv2 with Classpath Exception — fully free for commercial use, no field-of-use restrictions. Amazon Correto License & cost GPLv2 + Classpath Exception Free for commercial use 100% open source Support AWS cloud customers only Commercial support available only through AWS Support plans, covering AWS-hosted workloads only. LTS versions Java 8 Java 11 Java 17 Java 21 Java 25 JDK and JRE builds for Linux, Windows, and macOS. PSU security updates only. Features & special flavors ACCP crypto provider Containers 176.07MB on Alpine Linux base Pre-built images for Alpine Linux, Amazon Linux, and Debian. Available on Docker Hub and Amazon ECR. Buildpacks Yes Hardened images Via 3rd party No first-party hardened images. Hardening, SBOM generation, and image signing depend on third-party providers. Amazon Corretto is a free, TCK-verified OpenJDK distribution maintained by Amazon Web Services, a major cloud provider and member of the GraalVM Project Advisory Board and the JCP EC. Amazon Corretto is available on Linux, Windows, and macOS, with JDK and JRE builds for all supported LTS versions: Java 8, 11, 17, 21, and 25. AWS provides regular updates for all LTS versions and a current release and applies urgent off-cycle fixes when available. There is no CRaC support, no GraalVM or native image compilation, and no performance-optimised build variant. One differentiating feature of Corretto is the Amazon Corretto Crypto Provider (ACCP). It’s a cryptographic library optimized for AWS infrastructure that can boost the performance of cryptographic operations, particularly in earlier Java versions. It comes with a tradeoff. As an alternative implementation of OpenJDK’s cryptographic algorithms, its behavior may differ from standard OpenJDK. Teams running Corretto outside AWS, or relying on cryptographic behaviour for compliance reasons, should evaluate ACCP's suitability independently. JavaFX is no longer included in or supported by any Corretto release. It was initially available in Amazon Corretto 8 on select platforms only. Amazon halted its support on March 31, 2026. The support model is Corretto's most significant practical constraint. Commercial support is available but exclusively through AWS Support plans, and only for workloads running on AWS Cloud. On-premises deployments, hybrid environments, and multi-cloud infrastructure fall entirely outside the support boundary. Amazon has stated it does not currently plan to launch standalone Corretto-specific support plans. For organisations not deeply committed to AWS infrastructure, this means Corretto is a community-only distribution in practice, regardless of Amazon's engineering involvement. On the container side, pre-built images are available on Docker Hub and Amazon ECR for Alpine Linux and Amazon Linux. There are no CDS-enabled images. Hardened container images, their SBOM generation, and image signing are not available directly from AWS. All three depend on third-party vendors. The Paketo Buildpack for Java is supported. For teams already running on AWS who want a free, AWS-aligned JDK, Corretto is a natural fit. For anyone evaluating a long-term Java platform strategy that extends beyond AWS, the support boundary is a hard constraint that typically pushes teams toward distributions with a more portable support model. Best for: Teams running entirely on AWS who want a free, cloud-provider-maintained JDK and are comfortable relying on AWS Support plans for their Java runtime. AWS Graviton users benefit from Corretto's ARM64 optimisations. Licensing: GPLv2 with Classpath Exception — fully free for commercial use, no field-of-use restrictions. Azul Zulu License & cost GPLv2 + Classpath Exception Azul License Community Availability (CA) and Subscriber Availability (SA). Only community builds are available for free. Support Commercial SLA available CA tier: community only, no indemnification. SA tier: commercial SLA with 24/7 follow-the-sun support. LTS versions Java 8 Java 11 Java 17 Java 21 Java 25 Legacy Java 6 & 7 JDK and JRE builds. 8 years of LTS support plus 2 years of Extended Support. Features & special flavors OpenJFX (FX bundle) IcedTeaWeb (Java Web Start) CRaC support Azul Platform Prime Containers 88.14MB on Alpine Pre-built images for Alpine, Debian, Ubuntu, CentOS, Rocky Linux, and Distroless. Available on Docker Hub (Docker Official Image), GHCR, GCR, Amazon Marketplace. Buildpacks Yes Hardened images Via 3rd party Zero-CVE hardening depends on third-party tooling — provenance and update cadence are not directly controlled by Azul. Azul Zulu is a free, TCK-verified OpenJDK distribution provided by Azul Systems, a member of the OpenJDK Vulnerability Group and the JCP EC. Azul engineers participate in fixing issues in the OpenJDK project, though they are not among its primary contributors. Azul offers two distinct types of builds, and understanding the difference matters for procurement: Community Availability (CA) — free to use, tested and TCK-certified, but without a guarantee of patent non-contamination. CA builds do not include commercial SLA, indemnification, or guaranteed patch cadence beyond the community schedule. Subscriber Availability (SA) — commercially supported builds, available as Azul Platform Core. SA builds are tested, certified, stabilized (security only), and include patent and non-contamination indemnification. In late 2024, Azul significantly restructured its pricing: the minimum entry-level contract rose from $6,000 to $41,000 per year. Mid-range tiers saw increases of 35–292% depending on vCore count, which pushed their customers to evaluate alternatives. As of late 2025, Azul no longer publishes its pricing publicly. The unlimited tier was previously listed at $483,000 per year. The company provides builds with OpenJFX (FX bundle), Mission Control, and IcedTeaWeb, which is an open-source implementation of the Java Web Start, useful for legacy enterprise applications that still depend on that technology. CRaC (Coordinated Restore at Checkpoint) support is also available, enabling faster startup for checkpoint-capable applications. Azul supports all current LTS versions: Java 8, 11, 17, 21, and 25. Legacy Java 6 & 7 versions extended support is available, but only as a commercial offering under the SA tier. It is not included in the free CA builds. LTS builds receive 8 years of support plus an additional 2 years of Extended Support, giving customers a runway to upgrade to a newer Java version without urgency. Quarterly CPU releases are provided on schedule to all users. Commercial clients receive 24/7 “follow-the-sun” support with direct access to Java engineers, and emergency and off-cycle patches under SLA. On the container side, Zulu images on Alpine Linux Docker Hub weigh 88.14 MB for the minimal build. Azul provides images across multiple Linux distributions, including Alpine, Debian, Ubuntu, CentOS, and Rocky Linux on GitHub. Images are available on GHCR, GCR, and the Amazon Marketplace. Azul does not provide first-party hardened container images. Hardening, SBOM, and image signing are available only through third-party vendors. Organisations seeking a single vendor for runtime, OS, and container support will need to source those components separately and manage separate support relationships for each layer. Best for: Organisations with existing Azul commercial relationships Licensing: Community (CA) builds under GPLv2 with Classpath Exception — free. Commercial (SA) builds under Azul commercial license — subscription required. Red Hat OpenJDK License & cost GPLv2 + Classpath Exception Community builds free Community builds are open source and free. Support For RHEL clients only 24/7 commercial support is not available as a standalone product. It is bundled with a Red Hat Enterprise Linux subscription, and limited to RHEL and Windows. LTS versions Java 8 Java 11 Java 17 Java 21 Java 25 JDK and JRE builds. PSU security updates only. Features & special flavors GraalVM via Mandrel Containers 146.7MB on Red Hat Universal Base Image (UBI) Images with CDS. Available via Red Hat Container Registry only. Buildpacks No Hardened images Yes Hardened via Red Hat Universal Base Image — built and maintained in-house. Red Hat is an active contributor to the OpenJDK project and a member of both the JCP EC and the GraalVM Project Advisory Board. Red Hat OpenJDK provides free builds for LTS Java versions 8, 11, 17, 21 and 25, with JDK and JRE builds available for all supported versions. All versions but JDK 8 are TCK verified. Platform support is the most significant constraint to understand before evaluating Red Hat OpenJDK for your environment. Builds and commercial support are limited to Red Hat Enterprise Linux (RHEL) and Windows. Commercial support is available with 24/7 service, ongoing patches, fixes, and updates, but it is not sold as a standalone product. Support is bundled with a RHEL subscription, which has two practical implications. First, organisations not running RHEL cannot purchase Red Hat Java support independently. Second, if the underlying RHEL version reaches its retirement date before the JDK support lifecycle ends, JDK support ceases at the RHEL EOL date, not the Java EOL date. Teams running Windows deployments without Red Hat middleware require an additional, separate subscription for OpenJDK on Windows. On the security update cadence, Red Hat OpenJDK ships PSU (Patch Set Update) releases only. It does not include the full Oracle CPU (Critical Patch Update) schedule. For GraalVM-based native image compilation, Red Hat offers Mandrel, a downstream distribution of GraalVM Community specifically focused on native image support for Quarkus applications. Mandrel is not a general-purpose GraalVM replacement. It is tightly scoped to the Quarkus ecosystem and does not provide the full GraalVM feature set. There is no CRaC support, no performance-optimised flavour, and no OpenJFX bundle. Red Hat provides first-party hardened container images via the Universal Base Image (UBI), with SBOM included and image signing enabled natively. Images also include Class Data Sharing (CDS) support for improved startup performance. The tradeoff is that images are tightly tied to the Red Hat ecosystem and available only via the Red Hat Container Registry. For those who don’t use Dockerfiles, there is no Paketo Buildpack support. For teams already running within the Red Hat ecosystem, Red Hat OpenJDK is a natural and well-integrated choice. OpenJDK entitlements can be included in the RHEL subscription. For anyone outside that ecosystem, the coupling to RHEL and the absence of standalone support options make it a difficult case to build. Best for: Organisations already running on the Red Hat ecosystem Licensing: Community builds under GPLv2 with Classpath Exception — free. Commercial support requires a Red Hat Enterprise Linux subscription. Microsoft Build of OpenJDK License & cost GPLv2 + Classpath Exception Free for commercial use Open source. Released in 2021 primarily to support Microsoft's Azure ecosystem. Support Azure customers only Commercial support limited to Microsoft Azure cloud workloads. Available via Microsoft Azure support plans. LTS versions Java 11 Java 17 Java 21 Java 25 JDK and JRE builds for Linux, Windows, and macOS. Quarterly CPU updates for supported versions. Features & special flavors No Containers N/A Buildpacks No Hardened images N/A Microsoft released its own free, long-term supported OpenJDK distribution in 2021. Microsoft participates in the OpenJDK community and holds membership of JCP Executive Committee and the Adoptium Working Group. Microsoft OpenJDK builds are tested against the Eclipse Adoptium Quality Assurance suite and have passed the TCK verification. The distribution provides JDK and JRE builds for LTS versions 11, 17, 21 and 25, available on Linux, Windows, and macOS with quarterly CPU updates aligned to the OpenJDK schedule. One aspect of the Microsoft build worth understanding carefully is that the binaries may include fixes that have not yet been backported upstream into OpenJDK, and there is no guarantee that those fixes will ever be integrated into the main project. In practice, this mirrors the same concern noted for SapMachine. If Microsoft-specific patches remain outside the OpenJDK codebase, migrating away from the Microsoft build in the future may require identifying and replicating those fixes manually in a different distribution. Teams with long-term planning should factor this into their evaluation. The commercial support picture follows the same pattern as Amazon Corretto: support is available, but only for workloads running on Microsoft Azure. For teams running Java on Azure who already have a Microsoft support relationship, this is a practical and low-friction option. For anyone else, it is a community-only distribution in everything that matters operationally. Best for: Development teams working in Azure-centric environments Licensing: GPLv2 with Classpath Exception — fully free for commercial use. IBM Semeru Runtimes License & cost GPLv2 + Classpath Exception Community builds for free. Support Yes IBM commercial support available for LTS versions across Linux, macOS, and Windows. IBM also offers commercial support for Eclipse Temurin - one of few vendors to back a third-party distribution. LTS versions Java 11 Java 17 Java 21 Java 25 JDK and JRE builds. Features & special flavors Eclipse OpenJ9 JVM Containers N/A Buildpacks No Hardened images No IBM Semeru Runtimes is the only distribution in this comparison that is not built on the HotSpot JVM. IBM contributes to the OpenJDK project and holds a seat on the JCP Executive Committee. Its engineers developed Eclipse OpenJ9, its own Java Virtual Machine, as an alternative to HotSpot JVM used by every other distribution in this comparison. IBM Semeru Runtimes combines OpenJDK class libraries with the OpenJ9 JVM, producing a TCK-verified distribution that is API-compatible with the Java SE specification but architecturally distinct at the runtime level. What OpenJ9 means in practice: OpenJ9 is designed for low memory footprint and fast startup time rather than maximum sustained throughput. In workloads that start frequently, run briefly, or operate under strict memory constraints, like microservices, serverless functions, container-dense deployments, OpenJ9 can outperform HotSpot on those specific metrics. But that’s not always the case, see our HotSpot vs OpenJ9 performance study. The tradeoff is compatibility. Because OpenJ9 is a different JVM implementation, some tooling, JVM flags, garbage collector tuning parameters, and diagnostic tools that work with HotSpot may not behave identically under OpenJ9 — or may not be available at all. IBM provides commercial support for LTS versions of IBM Semeru Runtimes. Notably, IBM also offers commercial support for Eclipse Temurin, making it one of the few vendors willing to back a third-party distribution. Best for: Organisations with existing IBM infrastructure or OpenJ9 expertise. Licensing: GPLv2 with Classpath Exception — community builds free. SapMachine License & cost GPLv2 + Classpath Exception Free for commercial use Maintained primarily to support SAP's own product ecosystem. No commercial licensing tier. Support SAP BTP customers only SapMachine is supported as part of the SAP Business Technology Platform (SAP BTP). No independent commercial support offering for non-SAP workloads. LTS versions Java 17 Java 21 Java 25 JDK and JRE builds for Linux, Windows, and macOS. Features & special flavors SAP JVM S AP BTP integration Containers N/A Buildpacks No Hardened images N/A SapMachine is a free, TCK-verified downstream distribution of OpenJDK version maintained by SAP, a major contributor to the OpenJDK project and a member of both the OpenJDK Vulnerability Group and the JCP Executive Committee. The distribution provides JDK and JRE builds for LTS versions 17, 21 and 25, available for Linux, Windows, and macOS. All releases, including quarterly ones, are aligned with the OpenJDK schedule. SapMachine exists primarily to serve SAP's own product ecosystem, not as a general-purpose Java runtime for arbitrary workloads. SAP uses SapMachine as the JDK underpinning its Business Technology Platform (SAP BTP). The distribution receives targeted fixes and patches relevant to SAP's environment, some of which may not be accepted upstream into OpenJDK. That last point has a practical implication worth stating plainly. If patches specific to SapMachine do not make it into OpenJDK, migrating away from SapMachine to another distribution in the future may require effort to identify and replicate those fixes in a different JDK. SAP also develops SAP JVM — a separate, proprietary JVM with additional features optimised specifically for SAP environments. SAP JVM is distinct from SapMachine and is not an open-source product. Commercial support is not available as a standalone offering. SapMachine is supported within the SAP Business Technology Platform for SAP customers. Best for: Organisations running SAP Business Technology Platform workloads that need a supported, SAP-aligned JDK. Licensing: GPLv2 with Classpath Exception — fully free for commercial use. Comparative table of Java runtimes Below is a summary of the key features and options the most commonly used JDK vendors offer. Download the full table to see all 18 features compared across six major distributions in one place. Feature Liberica JDK Oracle Azul Zulu Red Hat Corretto Eclipse Temurin Recommended by Spring Free for commercial use Partial (BCL, OTN, NFTC) 100% open source, no field-of-use restrictions Free LTS versions: Java 8, 11, 17, 21, 25 Partial (Java 8 paid) Java 6 & 7 extended support JDK build for containers Liberica JDK Lite Performance flavour for Java 8 & 11 Liberica JDK Perf Partial (Java 8 paid) OpenJFX GraalVM Java AOT compiler JDK with CRaC support Java Flight Recorder and Mission Control Builds for a wide variety of platforms Free CPU & PSU security updates Free for personal use PSU Only PSU Only PSU Only Minimal containers 45.95 MB (on Alpaquita Linux) 307.56 MB (Oracle Linux 9) 88.14 MB (on Alpine Linux) 146.7 MB (UBI) 176.07 MB (Alpine Linux) 71.81 MB (Alpine Linux) Paketo Buildpack for Java or Buildpack Spring Boot (Default) Hardened Buildpack Container Registry Docker, GHCR, MCR, GCR, Amazon ECR Oracle CR Docker, GHCR, GCR, Amazon Marketplace RedHat CR Docker, GHCR, Amazon ECR DHI, CG, Amazon ECR TCK verification Patent Grant Performance Parity with Oracle Java SE Multiple Installers & Packages (tar, deb, MSI, DMG, JDK/JREs) No apk No pkg No JRE No dmg No deb No apk No rpm No pkg No dmg Discovery API, Package managers, Docker Hub No Discovery API No Discovery API Java Web Start and Applets No Applets OpenWebStart for JWS No Applets IcedTeaWeb for JWS License GPLv2 + Classpath BCL, OTN, NFTC GPLv2 + Classpath GPLv2 + Classpath GPLv2 + Classpath GPLv2 + Classpath Contribution to OpenJDK project Commercial support available Commercial support available For AWS CLoud clients only Via 3rd parties Ready to migrate from Oracle and get more features, tools, and solutions with more affordable support? Consult BellSoft experts — our engineers will answer any of your questions and help with the migration. Contact us Frequently Asked Questions What is the best free alternative to Oracle Java? The right answer depends on your environment. Liberica JDK by BellSoft is a strong choice for most teams, especially those migrating from Oracle JDK or Azul Zulu. It offers a wide range of supported versions, platforms, and architectures, and places a strong focus on performance and security. It is the only distribution recommended by Spring. Eclipse Temurin is the best option for teams that prioritise vendor neutrality and community governance. For teams embedded in a specific ecosystem, Red Hat OpenJDK, Amazon Corretto, IBM Semeru, and the Microsoft Build of OpenJDK each serve their respective platforms well. All are free for commercial use under GPLv2 with Classpath Exception. Is OpenJDK really free for commercial use? Yes. OpenJDK and all major vendor distributions that are based on it, like Liberica JDK, Eclipse Temurin, Amazon Corretto, are licensed under GPLv2 with Classpath Exception, which permits free commercial use without restriction. There are no per-user, per-processor, or per-employee fees. What is the difference between OpenJDK and Oracle JDK? OpenJDK is the open-source reference implementation of Java SE, built from public source code. Oracle JDK is Oracle's proprietary build of OpenJDK, with additional tools and a commercial license. Since Java 17, Oracle JDK has been available under the Oracle No-Fee Terms and Conditions (NFTC), but with restrictions, particularly for production use of older LTS versions. Vendor-supported OpenJDK distributions like Liberica JDK are TCK-certified, commercially supported, and free for commercial use without Oracle's licensing constraints. Which OpenJDK distribution is recommended for Spring Boot? Liberica JDK by BellSoft is the only OpenJDK distribution officially recommended by the Spring team on spring.io. Liberica JDK is the default runtime in the Paketo Spring Boot Buildpack. How do I replace Oracle Java without paying for a license? Download any free OpenJDK distribution. Visit bell-sw.com/pages/downloads/, select your Java version and platform, and install. For containers, replace your Oracle-based base image with a Liberica JDK image from Docker Hub. Migration is drop-in compatible for standard Java SE workloads — no code changes required. See the full migration guide for a step-by-step walkthrough. What happened to Oracle Java licensing in 2023? In January 2023, Oracle replaced its Java SE subscription pricing with the Java SE Universal Subscription, which charges based on total employee headcount, including employees who do not use Java. This change meant that most organisations saw their Java licensing costs increase substantially, and introduced significant compliance complexity. It was the primary driver of accelerated migration to open-source JDK alternatives through 2023–2025. Is Liberica JDK free? Yes. Liberica JDK is free for commercial use under GPLv2 with Classpath Exception. There are no field-of-use restrictions, no per-user fees, and no employee-count pricing. BellSoft also offers commercial support for organisations that require SLA-backed engineering support, but the JDK itself is always free. Which JDK distribution has the most platform support? Liberica JDK supports a wide range of operating systems and CPU architectures: Windows, macOS, and 14 Linux distributions (both musl and glibc), Raspbian, and Solaris 10 and 11. Supported CPU architectures include x86 (32- and 64-bit), ARM (32- and 64-bit), PowerPC, RISC-V, and SPARC. See the up to date list of supported configurations here. What is a hardened JDK container image? A hardened JDK container image is a minimal image that has been stripped of unnecessary packages to reduce the attack surface, continuously monitored for new vulnerabilities, and patched to maintain a zero or near-zero CVE status. Hardened images come with a Software Bill of Materials (SBOM), image signing, and other provenance artefacts that supportf clean software supply chain compliance. Commercial hardened image offerings, such as BellSoft Hardened Images, also include an SLA for CVE remediation, guaranteeing a defined response time when new vulnerabilities are disclosed. Among the distributions in this comparison, only Liberica JDK and Red Hat OpenJDK offer first-party hardened images — all others depend on third-party vendors. How much does commercial Java support cost? Costs vary widely. Oracle JDK licensing under the Java SE Universal Subscription is based on employee headcount and can reach hundreds of thousands of dollars annually for large organisations. Azul restructured its pricing in late 2024, raising the minimum contract from $6,000 to $41,000 per year, and no longer publishes pricing publicly as of late 2025. Red Hat support is bundled with RHEL subscription costs and is not available as a standalone product. BellSoft Liberica JDK commercial support is available at stable, publicly stated pricing. Contact BellSoft for a quote. - [Application cost reduction with Arm servers](https://bell-sw.com/blog/application-cost-reduction-with-arm-servers/): This article covers the topic that may seem unusual, namely the Arm servers. For most developers, Arm is a dark horse in the world of server-class hardware dominated by Intel and AMD. But in truth, Arm servers have been around for a decade and are becoming increasingly popular thanks to their improved performance coupled with low maintenance costs. Let’s look into their capabilities and see how your applications can benefit from migrating to this architecture. This article follows my talk on JRush Episode 4 dedicated to modern Java development for banking and FinTech. If you prefer watching a presentation to reading the article, you can register for free and enjoy three presentations by leading IT experts. You will also gain access to the previous episodes covering the latest trends and cutting-edge solutions in the Java industry. Watch Jrush for free Table of Contents What is ARM architecture? Arm evolution Why Arm servers? Java on Arm Performance of Arm servers Reducing cloud costs with Arm and Liberica Lite Conclusion What is ARM architecture? The Advanced RISC Machine (ARM) processors form a family of Central Processing Units (CPUs) based on the RISC instruction set architecture (ISA). ISA provides an interface between software and hardware specifying how the software can manage the CPU. It defines the supported instructions, data types, registers, memory management, etc. RISC ISA is a reduced instruction set computer architecture, which uses smaller and simpler instructions given to the computer to perform its tasks as opposed to CISC, which utilizes complex instructions. As a result, each program utilizes several instructions, which are rapidly accomplished, making Arm processors highly performant and significantly more power-efficient than Intel processors. Intel and AMD use the AMD64 (also known as x86-64) instruction set with various extensions. To run a program on an Intel or ARM processor, we need to recompile it with a compiler. The same is true for managed runtimes that compile the code written in one of the high-level languages to native machine code that can be executed by the processor. The first properly working samples of Arm processors appeared in 1985. Arm architecture rapidly gained popularity. To compare: 30 billion processors were shipped in 2013, but by 2022, this number reached 230 billion. Remarkably, ARM Ltd. doesn’t manufacture the chips. Instead, it licenses the rights for manufacture to other companies who build their own products based on this design. Due to minimal power consumption, lower development costs, and smaller chip size, Arm processors are primarily used in smartphones and embedded systems. But in recent years, Arm started conquering other fields such as Personal computers (Apple M1 chips) and servers; High-performance computing (the Fugaku supercomputer is based on the Arm architecture); Cloud computing; Edge computing. Arm evolution The Arm ecosystem extends on a regular basis. The Arm specifications are constantly enriched with new features introduced as extensions. Some of them are optional, so you can choose a CPU exactly for your needs. At the same time, older extensions can also be supported. This flexibility is Arm’s distinctive advantage. The graph below depicts the evolution of the specification for ARMv8-A and v9-A (application architecture profile): ARM Specification Progression In 2021, the next-generation Arm CPU Armv9 was launched, the first new Arm architecture in a decade. It boosts increased security with the Arm Confidential Compute Architecture, as well as enhanced ML and AI capabilities. The new processor is promised to accelerate the transition from general-purpose to more specialized processing across all IT fields. Arm architecture is supported by numerous software technologies, including Operating systems (all major Linux distributions, including Debian, Ubuntu, RHEL, openSUSE, and OEL). We are currently working on adding Arm support to our Alpaquita Linux, a minimalistic distro with the base image of 3.32 MB (with optimized musl) and 8.4 MB (with glibc); Middleware such as OpenJDK, Elasticsearch, Apache Kafka, Apache Spark, and Elastic Cassandra; Container technologies — Docker and Kubernetes; Virtualization solutions, for instance, KVM; Data management systems like Hadoop, MySQL, MariaDB. And many other tools. But the most important thing is that cloud adoption of Arm architecture proceeds by leaps and bounds. Arm-based systems power the instances in Amazon, Oracle Cloud, Azure, and other cloud environments. It is also possible to buy physical servers developed by Ampere. Moreover, Kubernetes works on Arm and enables the creation of heterogeneous clusters, so you can spread the workloads across various architectures based on your business needs. For instance, Apache Pulsar, a highly scalable and low-latency messaging platform has recently got an official container image for linux/arm64, so you don’t have to use emulation that affects the performance. Why Arm servers? Why would enterprises invest into migrating their data centers to Arm-based servers from tried-and-true Intel which has dominated this field for decades? The answer is: long-term cost efficiency. As the amounts of data processed by large enterprises increase in a geometric progression, data centers literally become the hot spots of IT infrastructure. On the one hand, servers get faster and more powerful to handle heavier loads, but on the other, they devour more space and power. Data centers also have to be cooled and maintained, and increasing the density of machines only deteriorates the situation. These issues raise the demand for smaller, flexible, low-energy, and yet performant processors. This is where Arm comes into play with the following substantial advantages: Lower power consumption and relatively low Thermal Design Power (TDP) compared to similar Intel solutions; High flexibility to choose an optimal processor for various tasks and modularity to design custom processors tailored to specific workloads. The space savings go even further, because you can divide physical servers into numerous virtual machines or rent the VMs in the cloud; Healthy competition on the Arm market stimulation innovations and providing end-users with affordable technologies; Excellent performance of server-class Arm processors. Regarding the last bullet point — how powerful can an Arm processor be? The AmpereOne™ 64-bit multi-core processors can have up to 192 single-threaded cores and are capable of handling the densest deployments and the most demanding compute installations in the world. At the same time, this processor family provides the most cores per rack, allowing enterprises to save space, power, and reduce carbon footprint. Therefore, you can select optimal Arm processors for any need, be it ultra-low-power embedded systems, high performance computing, statistical and financial math computations, or AI/ML-workloads. Java on Arm The BellSoft engineers were among the first who saw great prospects for Java on Arm and took an active part in making Java a perfect fit for this architecture. Namely, they enhanced the performance of the AArch64 port of OpenJDK by proposing and integrating JEP315: Improve Aarch64 Intrinsics. And today, BellSoft is one of the most active contributors to the AArch64 port of OpenJDK. If your application is based on Java, you can consider migrating the workloads to Arm architecture, because Java Armv8 port is stable and production grade. It supports all major JVM features, including all mainline Garbage Collectors and Shenandoah, so you can develop, deploy, and monitor your Java applications on Arm architecture without challenges. You can read more about the performance of Java Arm port as compared to other architectures in our dedicated article. Having a native JVM for a given architecture is essential for application performance. When we introduced native Liberica JDK builds for Arm-based Apple Silicon chips in 2021, we benchmarked two JDK implementations: macOS-x86_64 on Rosetta 2 and macOS-aarch64. The results demonstrated almost two times the difference in performance for some workloads! Therefore, if you want to get the maximum out of Arm servers, select a runtime that supports this architecture, such as Liberica JDK that runs natively on Arm. Download Liberica JDK Performance of Arm servers Arm servers are getting more powerful with each version. Let’s look at the comparison of three Graviton generations offered in AWS to trace the evolution of the architecture: Evolution of Arm servers in AWS According to Amazon post-marketing study, customers utilizing the Graviton3-based C7g instances powered by Graviton3 noticed up to 40% performance improvement compared to C6g instances running on Graviton2. At the same time, Java is getting more performant, too. There have been twelve JDK releases since Java 8, the most popular version, and each of them contains new features targeted to optimize memory consumption and increase Java speed. Let’s now compare the performance of JDK 8 and 20 on the cloud Arm servers using the DaCapo benchmark: Benchmarking JDK 8 vs 20 on Arm servers The data above demonstrates that we can reach more than 30% increase in app performance using the latest versions of Java and Arm servers. Reducing cloud costs with Arm and Liberica Lite Enhanced performance always comes handy, and power-efficiency helps to save budgets in the long term, but what if a company is looking for immediate cost reduction seeing that its cloud bills inflate uncontrollably? Arm servers can be a remedy in this situation. A comparative analysis of Arm, AMD, and Intel costs in the Amazon cloud has shown that Graviton2 processors can be significantly more cost-efficient than other platforms. The study compared the 16xlarge instances based on the m6g (Graviton2, Arm), m5a (EPYC1, AMD), and m5n (Xeon Cascade Lake, Intel) for the 64-vCPU count. Not only are the Arm-based instances cheaper than AMD and Intel, they can achieve 40% better performance per dollar when translating the time to completion of SPEC tests to hours and multiplying the result by hourly cost. You can also reduce cloud costs by migrating to Java microcontainers and reducing memory consumption. For that purpose, BellSoft provides Liberica Lite optimized for memory footprint in the cloud environment. Containers with Liberica Lite can be 1.5 smaller than those based on other popular Java runtimes. What is more, we added the support for Aarch64 as part of our efforts in porting Alpine Linux to OpenJDK, so now developers can use our smallest containers on the market of only 42 MB on this architecture, too! If you want to know more about the technical capabilities of Liberica JDK and how it will enable you to unify the whole corporate Java stack, download the white paper by clicking on the button below. Get Liberica JDK white paper Conclusion Arm architecture thrives and reaches new heights every year. Most importantly, there is no shortage of software tools and platforms that function consistently on Arm. So if you have a cloud-native Java application, consider migrating it to this power-efficient and highly performant architecture — as we found out, Java and Arm make an excellent combo! - [New version of Liberica NIK 23.0 with ParallelGC is available](https://bell-sw.com/blog/new-version-of-liberica-nik-23-0-with-parallelgc-is-available/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 23.0 with several enhancements and new features. New features and enhancements Summary of improvements in Liberica NIK 23.0: Add JFR ThreadCPULoad event implementation; Thread user cpu time implementation for LinuxThreadCpuTimeSupport.getThreadCpuTime() method; URLConnection.getContentType() returns null for html resource; Remove type checks from JNI-to-Java call stubs, which can break compatibility; Add support for TrayIcon on macOS and Windows. Note that if you run a TrayIcon app on macOS, you need to add the Info.plist file: CFBundleIdentifier nativeimage.trayicon.sample But the most impactful new feature is the integration of the ParallelGC implementation. Read on to find out more about the project where BellSoft engineers took active part and the benefits it will bring to AOT-compiled programs. Integration of ParallelGC Two major advantages of GraalVM are enhanced JIT compiler and AOT compilation that provides almost instant startup time of applications (native images), which is critical for specific cloud services such as Lambdas. But GraalVM CE will become even more powerful, because native images will have significantly lower latency thanks to a new Parallel GC integrated to Liberica NIK (GraalVM CE-based native-image compiler) as an experimental feature. Justification Up until now, GraalVM Native Image technology has used only SerialGC in GraalVM Community Edition (a single-threaded stop-the-world garbage collector) and G1 (Garbage-First) GC in Enterprise Edition. SerialGC freezes all threads while performing the collection (hence “stop-the-world”) and works in one thread itself leading to increased latency. Latency is a critical performance indicator that greatly affects user experience and justifies the integration of an additional low-latency GC to GraalVM CE to fully utilize available multiprocessor hardware and reduce pause times. It should be noted that when Graal compiler turns JVM-based code into a platform-dependent native executable, it runs on SubstrateVM, which is a partial JVM implementation. Garbage Collector in SubstrateVM is written in Java, so it would be hard to port to it the existing HotSpot ParallelGC, which is implemented in C++. Therefore, the team of developers focused on extending the SubstrateVM with a framework that enables the parallelization of GC work. In the following section we will explain how it was achieved. Implementation of ParallelGC to SubstrateVM As opposed to SerialGC, ParallelGC uses several threads to scan the objects, hence the accelerated GC procedure. The default setting is one thread per core, max amount is eight per core, so we can regulate the number of threads with the -XX:ParallelGCThreads option. To activate ParallelGC, use the --gc=parallel flag. The parallelization of GC in SubstrateVM will be performed in several steps. The first step was to parallelize the phase computing transitive closure, which contributes most to the GC pause time. For this purpose, we need to partition this work into tasks, which will be performed by individual worker threads. The threads are activated at startup, which affects the startup time slightly. ParallelGC reuses most of the SerialGC code and allocates memory in thread-local fashion to minimize interference between worker threads. Our ParallelGC keeps a busy worker thread counter. It means that when worker threads block, they decrement the counter, and increment it if they wake up. When the counter drops to zero, it means that all work has been done. The last active worker thread signals the completion of the parallel phase before blocking, and the rest of the garbage collection routine executes in a single thread. Worker threads remain blocked until they are woken up by the next garbage collection cycle. Results and future plans To validate the ParallelGC implementation, we used the HyperAlloc benchmark developed by the Amazon Corretto team, a workload simulating application characteristics that affect garbage collector latency. Experiment prerequisites: Intel i7-8565U CPU, 8 cores at 2 GHz, Ubuntu 20.04. The figures below demonstrate the results obtained using 8 worker threads and different heap sizes. SubstrateVM GC pause times with 32 Gb of heap memory SubstrateVM GC pause times with 2 Gb of heap memory SubstrateVM GC pause times with 512 Mb of heap memory The graphs above demonstrate the reduction of GC pause time by 10-40%. An improvement over SerialGC was also obtained with smaller numbers of GC threads. The next step within our project is to make it possible to execute more GC phases in parallel. The most promising candidates for implementation are incremental collections, root set scanning, and post-GC cleanup. The graph below shows the scalability of ParallelGC in Substrate VM. SubstrateVM GC pause times with different worker thread counts Conclusion Integration of ParallelGC to GraalVM CE will enable companies to benefit from Native Image technology and at the same time, improve essential KPIs. The BellSoft engineers will continue contributing to the enhancement of GraalVM technology. Right now, ParallelGC is available for macOS and Linux as an experimental feature not intended for production use. We encourage you to try it out and report any bugs discovered in the process via our GitHub page. For that purpose, download the new version of Liberica NIK 23.0, which is already available in the Download Center. Download Liberica NIK - [GPLv2 + Classpath Exception: the intricacies of open-source licensing](https://bell-sw.com/blog/gplv2-classpath-exception-the-intricacies-of-open-source-licensing/): According to research by The University of Law, 68% of people either don’t read or don’t understand the contracts they sign. We dare assume that the situation is even worse in the open-source software development world. Developers pull dependencies or projects from public repositories, such as Maven, Docker Hub, and GitHub, without reading the licenses. This negligence may lead to grave legal consequences for them or the enterprises they work for. Take Java, for example. The OpenJDK code is licensed under GPLv2 + Classpath Exception. What does it imply? What are the benefits of this license? Does it impose any legal risks for corporate Java development? This article will look into the intricacies of open-source licenses, including GPLv2. It doesn’t give legal advice and only provides a general overview of the topic. Table of Contents The importance of software licenses for developers Special aspects of open-source licensing OSI and FSF Key principles of the GPL license GPL and OpenJDK How to eliminate the risks related to non-GPLv2 + CPE code in OpenJDK The importance of software licenses for developers Software licensing may seem like a tedious legal technicality of no interest to developers, but this technicality is a cornerstone of the Open Source world. Licenses regulate the way developers interact with each other. When utilizing your colleagues’ code, you must meet their software’s license requirements (legal compliance). Violating licensing conditions may lead to lawsuits, which will devour your or your company’s time and money. On the other hand, licenses also help you protect your intellectual property. Rights and obligations imposed by different software licenses form a specific community. There are plenty of such communities worldwide: apart from open-source software, any platform or proprietary technology can have its user group interested in its evolution. Therefore, choosing a license means choosing friends, colleagues, and even the career path for many years ahead. After all, developing financial algorithms in banking or building open-source web frameworks in Java belongs to two different fields. Special aspects of open-source licensing Most third-party code you use is licensed. Closed proprietary code is distributed under EULA (End User License Agreement) written by the vendor, whereas open software utilizes one of the Open Source Initiative or Free Software Foundation licenses. We will talk about the differences between the two organizations later on. Software licenses are classified by the types of intellectual property they protect: copyright, patents, and trademarks. In general terms, copyright protects works of authorship and permits or prohibits using the code. Patents protect inventions or processes and help preserve the rights for implementing a mechanism not necessarily tied to a specific code. A trademark is a symbol, word, or phrase registered by a company or a community so nobody can pass a random fork for their work. Most open-source licenses are associated with copyright and patents and don’t give rights to trademarks. In other words, they form communities working on the same codebase utilized for various software products. For instance, there is a shared codebase in Java — OpenJDK, and numerous technologies built around it, including Oracle Java, Liberica JDK, etc. The coexistence of different open software communities gave birth to two related ideologies, Open Source and Free Software. OSI and FSF When we say “Open Source,” we usually mean the definition published on the OSI (Open Source Initiative) website, consisting of ten fundamental rules. OSI was founded in 1998 by Bruce Perens and Eric S. Raymond. Perens is famous for leading the Debian project and creating BusyBox and Linux Standard Base. Raymond is the editor of Jargon File and author of The Cathedral and the Bazaar, The Art of Unix Programming, and other books. FSF (Free Software Foundation) was founded in 1985 by Richard Stallman, who also launched the GNU Project and wrote the GNU General Public License (GPL). By “free” software, Stallman means the software that respects users’ freedom to run, copy, distribute, study, change, and improve the software. Both OSI and FSF advocate free software whose source code can be modified by any developer. But OSI’s approach is more flexible. While FSF promotes the idea that all software should be open-sourced without any restrictions on its use and modification, OSI is more pragmatic. OSI-approved licenses don’t oblige that all software distributed with open software must also be open source. These organizations were created by legendary developers whose work laid the foundation for modern open-source development. Years of experience enabled them to establish best practices for building open systems and communities. With the aid of legal advisers, these practices turned into clear licenses that any developer can apply. There are currently dozens of open and free licenses, and although they follow the OSI or FSF principles, each has its own nuances. Both organizations approve some licenses, some by OSI or FSF only. Every time you decide to use a new library in your project, read its license, then read the commentary in the FSF registry, or verify its compliance with basic principles if it is not on the list. Large companies usually have a legal department to deal with such tasks. Still, in some cases, it is evident from the first lines of the text that the license violates many principles, and there’s no need to waste the lawyer’s time on it. Key principles of the GPL license Almost all OpenJDK code is licensed under GPLv2 + Classpath Exception. But before delving into its features, let’s look at its ancestor, the GNU General Public License, which is a set of free software licenses written by Stallman and aimed at securing four freedoms to Use the software for any purpose; Study the source code and change the software to suit your needs; Share the software with your colleagues; Share the changes you make to promote innovations and community evolution. GPL works virally to implement these rules in real-world scenarios, meaning that everything based on the GPL-licensed code must also be licensed under GPL. The viral nature of GPL is well thought through and functions perfectly in practice. These are wonderful rules for open scientific research, but they clash with the interests of many commercial organizations that don’t want to share their proprietary know-how with the rest of the world. One may point out that if a company has no desire to share its work, it should simply use another license. Unfortunately, there’s always a risk of accidentally implementing a piece of GPL code, forcing the enterprise to open the whole project. The risk is significant because each corporate application utilizes hundreds of open-source dependencies. However, we don’t believe that GPL is pure evil. On the contrary, it made software development easier, faster, and cheaper than two decades ago. The key is to master handling it in the right way. GPL and OpenJDK Java and OpenJDK are intended for any purpose, including commercial software. Therefore, the OpenJDK license had to consider the Free Software ideology and the needs of individual developers, major enterprises, state organizations, etc. The solution was found in the modified version of GPL, which is called GPLv2 + Classpath Exception (CPE). It includes the following remark: “... the copyright holders of this library give you permission to link this library with independent modules ... and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module.” This amendment enables you to use the OpenJDK and proprietary code in one application. You only have to put proprietary libraries into CLASSPATH, so there’s no legal obligation to open their source code. The license is easy to use and very efficient, that’s why Java became a predominant platform for corporate development. At the same time, GPLv2 + CPE helps OpenJDK remain one of the flagships of Open Source development. At the moment of writing this article, the JCP Executive Committee included 18 companies, among them Oracle, IBM, Intel, BellSoft, and other industry leaders. The GitHub repository contains 74,000 commits, and the number constantly increases, making OpenJDK one of the biggest open-source projects in the world. To conclude, OpenJDK licenses allow the creation of both open and proprietary Java-based products. They facilitate and accelerate application development and foster a highly efficient OpenJDK community that works incessantly on Java improvement. How to eliminate the risks related to non-GPLv2 + CPE code in OpenJDK Nevertheless, there’s a fly in OpenJDK ointment — not all OpenJDK code is licensed under GPLv2 + CPE. Some files in the project codebase have GPL only without Classpath Exception, which resurrects the problem of viral GPL code. Enterprises have to be 100% certain that all components of Java runtime that link with their code have the CPE. This is why BellSoft provides Liberica JDK builds, for which we scan the JDK files and ensure that all public Java SE API components that link to your app’s code have the GPLv2 + CPE license. We also provide patent and non-contamination indemnification. Both services are part of our enterprise support. Don’t get stuck in licensing cobweb — contact us, and we will help you protect your software. Contact us - [Spring container size: Why it is important](https://bell-sw.com/blog/spring-container-size-why-it-is-important/): Why does container size matter? For some developers, the question sounds similar to “Why don’t cows fly?” tempting to answer “just because” without reflecting much on why it is crucial. But many programmers, especially backenders, don’t even consider the issues hidden underneath heavyweight containers. Their logic: “It’s no concern of mine!” This article will explain why reducing the container size will benefit all teams, even backenders. Table of Contents Why Spring makes Java code lighter Minimize the effort Reduce the cloud cost Accelerate docker pull Decrease the cash size Facilitate the development process and stimulate experiments New extralight Spring containers Conclusion Why Spring makes Java code lighter Soon after the Java programming language emerged, the developers understood it was a perfect platform for complex systems. Indeed, developing and maintaining a huge monolith in Java was much easier than in other languages. But monolithic applications have a substantial downside, i.e., prolonged startup. A simple application could start in one minute, a complex enterprise one in ten. A portal with portlets could even load in 40 minutes! In addition, Java development required special hardware, and it took ages to grasp complicated frameworks and their specific issues. Then came the Spring framework, which is lightweight, fast, user-friendly, and based on tried-and-true programming patterns. The adoption of Spring across Java development coincided with the dawn of microservices, and slow, clumsy Java apps gradually became a thing of the past. But there’s no time to bask in the Spring sun. We must continue improving the development techniques, or the nightmare of a 40-minute startup will return. Even now, the developer’s laptop has a hard time under the weight of dozens of deployed containers and microservices. We can alleviate its suffering by migrating to a lightweight Liberica Runtime Container of only 40.32 MB — the resulting Spring container can be 1.5 smaller than an Oracle-based one. But what benefits will it bring exactly? Minimize the effort The most significant advantage of any innovative solution is the seamless integration into the existing workflow and instant results. Switching to Liberica Runtime Container, in most cases, requires changing only one line FROM openjdk to FROM bellsoft/liberica-runtime-container in the Docker file, and the container size will be immediately cut several times. Note that Liberica Runtime Container is based on Liberica Lite optimized for the cloud and Alpaquita Linux, which comes with two libc implementations: improved musl and glibc. So even if you use a musl-based distro, you can migrate to glibc-based Alpaquita and still save a significant amount of memory. So why not try it out? Reduce the cloud cost The cloud offers unique advantages such as easy scalability, universal data access, and big data management. But according to a study by Gartner, companies overspend up to 70% on cloud services. The expenses can sometimes inflate to such an extent that businesses start thinking about abandoning the cloud for good. Look at the article on Amazon’s failure with serverless by David Heinemeier Hansson, the creator of Ruby on Rails. The key takeaway is that bad code turns into inadequate infrastructure, which wreaks havoc in large-scale deployments. But what if companies go back to earth? For developers, it will mean an immediate many-fold increase in work. Instead of ready-to-go cloud solutions (such as Spring Cloud AWS) they will have to create their own from scratch or even give up automatization. We believe that there are better decisions than refusing cloud opportunities. Instead, enterprises should learn to manage the cloud resources and integrate cloud cost optimization strategy in the early stages of cloud migration. If you have already noticed the negative impact on your finances, decreasing the base container image will help you get back on track. After that, you can start optimizing your IT infrastructure — we prepared a white paper with steps on optimizing cloud costs, tested and proven to work. Download it now, and don’t let the cloud hang over your IT budget! Accelerate docker pull What is the difference between waiting for 140 or 600 MB to load? If you have a fast Internet connection, the difference is negligible. However, in the case of a slow or expensive connection, pulling a heavy image may take ages, and constant docker push/pull will become torture. Decrease the cash size How much cache on your computer is eaten by Docker? Run docker system df to find out. You will notice a RECLAIMABLE section, which shows the space taken by unused images. The docker system df -v will provide you with more detailed information. If you work with Docker but disregard clearing the disk, the results will shock you because you may have hundreds of gigabytes of trash on your machine. On the one hand, you can run docker system prune and forget about the trash. On the other hand, it may contain images you will require later, so you will have to load them again. But, as we discussed earlier, lightweight containers load rapidly, so it is better to load necessary images from time to time rather than foster compulsive hoarding. Facilitate the development process and stimulate experiments Minor problems cloud the blue-sky thinking. Imagine you are struck with a grand idea. If you have to divert your attention to housekeeping activities, there’s a high chance you will get bogged down in routine and won’t be able to return to your idea. The life of a developer can be filled with numerous chores. Apart from heavy, slowly loading containers, you may need to download several gigabytes of Maven packages, debug a couple of framework configurations, or deploy and test some incomprehensible Helm Charts. In a perfect world, developers should Manage the number of utilized libraries, Apply straightforward configuration methods, Control the complexity of dependencies and resource capacity of the selected solution, Or else the whole development process will quickly spin out of control. It is especially relevant in the Spring ecosystem, which includes 30+ projects and loads of related code on GitHub and Docker Hub. But at the same time, dealing with these tasks means “death by a thousand cuts” — none of them is complicated on their own, but taken together, they may devour all of your or your team’s free time. For instance, you want to share an image with your colleagues without uploading it to Docker Hub but as a .tar.gz file. What if that file weighs about 1GB? Imagine the psychological state of the team that has to download and unpack it manually! New extralight Spring containers A simple application may be relatively small compared to the whole JDK, a container with libraries, or an operating system. Still, enterprise programs are usually complex and require multiple dependencies, significantly affecting the final image size. Fortunately, Spring Boot 3 has baked-in support for GraalVM Native image. What does it mean for Java applications? GraalVM Native Image enables the Ahead-of-time (AOT) compilation, eliminating unused code and turning an app into a compact native executable file. As a result, there’s no need to pack the whole JDK into the image anymore, and provided that you have mastered the intricacies of using Native Image with Java, you will be able to optimize memory consumption and minimize startup time to 1/10 s. If you would like to try out the Native Image technology, you need to install a GraalVM distribution. The Spring team recommends Liberica Native Image Kit (NIK), a free GraalVM CE-based native-image tool used by default with Cloud Native Buildpacks. Download Liberica NIK We also provide a ready-to-go container image with Alpaquita Linux and Liberica NIK. Alpaquita Linux has a base image size of only 3.32 MB (optimized musl) or 8.39 MB (glibc). Try calculating how much memory you can save if the base OS layer takes up less than 10 MB and your AOT-compiled app — about 50 MB! Conclusion In this article, we examined several benefits of small Spring container images. Of course, downsizing the container size is not a sole solution but a substantial part of an IT infrastructure management strategy, together with the right choice of libraries and other tools. But all in all, a lightweight container image, for example, an Alpaquita Container created specially for Spring Boot , is guaranteed to facilitate, accelerate, and cheapen microservice development. - [Migration from Java 8 to Java 17](https://bell-sw.com/blog/migration-from-java-8-to-java-17/): Life after Java 8 is in full swing, with fast microcontainers, cloud-native deployment, and a more performant JVM. And although BellSoft supports JDK 8 until March 2031, there will come a day when you have to let Java 8 go. Besides, with a new 2-year LTS-release cadence, Java gets new features, and APIs are updated more often, making the migration process more challenging regarding compatibility. We understand that upgrading the Java version in a complex enterprise project is not a one-click task, yet, it is not mission impossible. This article will guide you through the changes you need to introduce related to Framework and dependencies versions, Deprecated, updated, or added features, Garbage collection amendments, New APIs, And highlight the most common issues that you may encounter after the migration. And by the way, there’s a one-click solution to the issue — read on to find out! Table of Contents JDK 8 vs JDK 17: Performance and security Key changes and enhancements up to JDK 17 Modularity Comprehensive support for containers Garbage collection changes JDK Flight Recorder and JDK Mission Control Java Virtual Machine Tool Interface (JVMTI) New JVM logging system JShell New HTTP/2 API Strongly encapsulated JDK internals GraalVM compatibility How to prepare for the migration from JDK 8 to 17 How to migrate the enterprise Java project from Java 8 1. Update Java version, dependencies, and tools 2. Modularize your application 3. Update Docker images 4. Add missing dependencies for removed Java APIs and modules 5. Fix access to internal APIs 6. Check for deprecated and removed JVM parameters 7. Convert legacy logging flags to a new system Feeling overwhelmed? BellSoft’s solution to Java migration problem JDK 8 vs JDK 17: Performance and security Newer JDK versions are significantly more performant than older ones because of numerous enhancements to the JVM and new features that help the developers increase the performance of their applications. Some performance enhancements include New low-latency Garbage Collectors ZGC and Shenandoah GC, Improved G1 GC, Dynamic AppCDS for faster application startup, Better support for containers for optimal resource consumption in the cloud. A comparison of JDK 8 to JDK 17 in terms of key performance improvements is summarized in the table below. OpenJDK 17 OpenJDK 8 GC Improved G1 GC, Parallel GC, Serial GC G1 GC, Parallel GC, Serial GC, CMS GC Low Latency GC ZGC, Shenandoah GC x NUMA-aware allocation ✓ x Compact Strings ✓ x CDS Dynamic AppCDS Basic CDS (since 8u40) Number of Intrinsics (x86_64) 107 62 Segmented Code Cache ✓ x JDK-specific changes Strings, NIO backports x In addition, JDK 17 is compatible with newer framework versions such as Spring Boot 3.x, which are also packed with improvements. As far as security is concerned, some vendors including BellSoft provide quarterly updates for JDK 8 with security patches. However, newer JDK versions include features promoting better security and integrity of the Java platform, including strong encapsulation of JDK internals and the Foreign Function and Memory API. Key changes and enhancements up to JDK 17 There were numerous new features added to OpenJDK since version 8. The section below highlights key changes. In addition, many components were deprecated, amended, or eliminated since Java 8, so we recommend checking the full list of removed tools and components and security updates to avoid issues with compiling the application on a newer Java version. Modularity Starting with Java 9, Java applications are organized as modules, i.e., groups of closely related packages and resources with a module descriptor file. Modularity allows for The creation of custom JRE images with only required modules, reducing the final container image size. This is especially useful for containerized environments. Better dependency management and increased security. Java 11 supports both module- and classpath-based configuration, but as you migrate to Java 17, you must modularize your application. Comprehensive support for containers Ever since JDK 9, Java has become increasingly container aware. Some notable changes include but are not limited to: Support for cgroup memory limits in container environments, Improved Docker container detection and resource configuration usage, Flexible heap percentage selection of available RAM, Updated CPU count algorithm, Container metrics, Cgroups v2: Container awareness, Alpine Linux port for OpenJDK. A few of these improvements were ported to JDK 8, but in general, newer Java versions feel much better in containers and enable developers to use cloud resources more efficiently. Garbage collection changes New low-latency GC implementations were implemented in the OpenJDK: Z GC and Shenandoah GC. You can read more about these collectors in the Guide to Java Garbage Collection. In addition, G1 GC is the default collector starting with Java 9. Other important changes: Removed CMS GC, New GC logs aligned with the improved JVM logging system (see below). JDK Flight Recorder and JDK Mission Control JFR and JMC, previously commercial features in Oracle Java, were released into open source and are now available for free with OpenJDK distributions. Java Virtual Machine Tool Interface (JVMTI) The addition of a native programming interface supporting various tools for JVM debugging, monitoring, and profiling enables the developers to inspect and control the state of JVM processes more efficiently. New JVM logging system A new logging system introduced in JDK 9 allows for unified command line options for all logging, classification of log messages by tags, dynamic logging at runtime using jcmd or MBeans, and so on. JShell The Java Shell tool is an interactive REPL (Read Evaluate Print Loop) tool for learning the Java language, exploring new APIs, and prototyping new code. It evaluates statements, expressions, and declarations as they are entered and immediately shows the results. New HTTP/2 API The new HTTP/2 API that allows for more flexible handling of HTTP requests. Strongly encapsulated JDK internals The goal of forbidding access to JDK internal components is to encourage developers to use standard APIs for easier upgrading in the future and increase overall code security. Developers who use internal components will have to rewrite their code significantly as these packages now belong to modules unavailable for public access. GraalVM compatibility JDK 17 also enables the developers to take advantage of GraalVM. GraalVM is a platform that provides an enhanced JIT compiler, AOT compiler, and tools for running multilingual projects. Experimental support for AOT features and Graal JIT was added to JDK 9 and removed from JDK 16. GraalVM now actively evolves as a separate project. Builds are available for JDK 11 (GraalVM CE only), 17, and the latest Java release. How to prepare for the migration from JDK 8 to 17 Migrating an enterprise Java application to a newer JDK release might be difficult, especially considering the number of changes introduced to the platform since Java 8. Here’s how you can approach the task: Identify libraries and frameworks in your project that depend on JDK 8. Using the list of removed components in JDK 17, evaluate how these changes will impact your code. Prepare the testing environments to debug possible issues. How to migrate the enterprise Java project from Java 8 1. Update Java version, dependencies, and tools First thing first, download and install the latest update of JDK 17. Oracle doesn’t provide free updates for Oracle Java 17 anymore as per licensing changes. But you can migrate to Liberica JDK. It is a free and 100% open-source Java runtime recommended by Spring with Free updates for version 17 until 2030, The smallest container images for Java applications based on Liberica JDK Lite optimized for cloud and minimalistic Alpaquita Linux. Base images for Alpine Linux are also available, JavaFX bundled in Liberica JDK Full Flexible commercial support with emergency patches and fixes based on strict SLA. The next step is to update all third-party libraries, build tools, IDEs, and a framework with associated dependencies to the latest version that supports JDK 17. For instance, the minimal Spring Boot version required for Java 17 is 2.5. But consider upgrading to Spring Boot 3.x, the first major release in 4.5 years with 44 new features, 100+ enhancements, and baked-in support for GraalVM Native Image. Indeed, updating all dependencies is easier said than done, but otherwise, your application will fail to start. If at some point after the updates you get an UnsupportedClassVersionError, it means you use the code compiled for the newer Java version, 18–20. After updating the project components, compile your application. Resolve the remaining issues based on the errors and warnings you see (some of them we address below). If the program starts successfully, run the tests to verify its behavior: you will likely see more inconsistencies (for instance, different locale data formatting due to changes introduced by JEP 252). 2. Modularize your application To organize an application into modules, use module declarations — special module-info.java files put into the project’s root. They contain the data required to build a module (name, dependencies, public packages, reflection permissions, services offered, and services consumed). When working with Java modules, keep in mind that: Module names shouldn’t conflict, so they must be unique. It is recommended to use the reverse-domain-name pattern to name modules. All the packages are private by default, so you need to explicitly specify the public packages you want to make accessible to the outer world. The same rule applies to reflection. Only modules with added exports clause make the public types in certain packages available for use by other modules. In other words, module B is readable when module A depends upon module B and exports it. In this case, public types of module B are considered accessible by the JVM. All the other modules are treated as internal and cannot be accessed from within. Split packages (two packages with the same name belonging to different libraries) are not allowed, meaning several modules cannot export the same package simultaneously. Circular dependencies (when two or more modules depend on each other to function) are also not allowed. 3. Update Docker images If you use Docker container images in production, update your Dockerfile as well. BellSoft provides a broad selection of Liberica JDK and JRE Docker images for the most popular Linux distributions, including Alpaquita Linux, a minimalistic 100% Alpine-compatible distro with two libc implementations, enhanced security and performance, and LTS releases. Together with Liberica JRE Lite, it enables the developers to build performant microcontainers. Change this line in the Dockerfile to move your app into a lightweight container with Liberica and Alpaquita: FROM bellsoft/liberica-runtime-container:jre-17-stream-musl Alternatively, follow this guide to compile your application in a container with Liberica JDK and shift it into another container with Liberica JRE (we strongly recommend utilizing JRE builds for deployment to minimize resource consumption). 4. Add missing dependencies for removed Java APIs and modules If you encounter a java.lang.NoClassDefFoundError or java.lang.ClassNotFoundException when running your application on Java 17, you probably use modules that no longer exist in the JDK. For instance, the following Java EE modules were removed from Java 11: xml.ws (JAX-WS, plus the related technologies SAAJ and Web Services Metadata), xml.bind (JAXB), activation (JAF), xml.ws.annotation (Common Annotations), corba (CORBA), transaction (JTA). These modules are maintained separately, so you should add them as dependencies to your project to resolve compilation issues. In addition, the JavaFX and Web Start technologies were deleted from JDK 11 as well. But you can use Liberica Full with LibericaFX, an open-source implementation of JavaFX. Builds with OpenWebStart (reimplementation of Web Start) are also available. Overall, numerous APIs were taken out of JDK up to version 17. To save time on manual tracing, use the jdeprscan tool, which scans a jar file for deprecated or removed APIs. jdeprscan identifies only Java SE APIs and won’t work with third-party libraries. You can give jdeprscan a jar file, a directory, or a separate class name. If the tool produces a message error: cannot find class … , adjust the classpath to include all dependent classes. But it is also possible that the file uses a removed API. For instance, let’s scan an aspectjweaver-1.9.9.1.jar: jdeprscan aspectjweaver-1.9.9.1.jar The output is: Jar file aspectjweaver-1.9.9.1.jar: class org/aspectj/weaver/bcel/ExtensibleURLClassLoader uses deprecated method java/lang/ClassLoader::getPackage(Ljava/lang/String;)Ljava/lang/Package; class org/aspectj/lang/Aspects14 uses deprecated method java/lang/reflect/AccessibleObject::isAccessible()Z error: cannot find class org/apache/commons/logging/LogFactory class org/aspectj/lang/Aspects uses deprecated method java/lang/reflect/AccessibleObject::isAccessible()Z error: cannot find class org/apache/commons/logging/Log class org/aspectj/weaver/loadtime/definition/DocumentParser uses deprecated class org/xml/sax/helpers/XMLReaderFactory class org/aspectj/util/FileUtil uses deprecated method java/io/DataInputStream::readLine()Ljava/lang/String; class org/aspectj/util/FileUtil uses deprecated method java/io/File::toURL()Ljava/net/URL; You can also use a --for-removal flag to limit the scan to APIs deprecated for removal. A --list parameter lists all deprecated APIs without any scanning. 5. Fix access to internal APIs Strong encapsulation introduced in Java 17 forbids the usage of reflection to access internal JDK components. If your code accesses non-public APIs, the application will throw an InaccessibleObjectException. JDK versions 9–16 allowed reflective access to JDK internal with the --illegal-access option, but as most libraries now use standard APIs, the option is no longer applicable in Java 17. To solve this problem, use the jdeps tool, a dependency analyzer. When used with a --jdk-internals option, it lists class-level dependencies in the JDK internal APIs. You can update the tools and libraries in question based on this information. If, for some reason, you can’t replace some libraries with newer versions, two options provide access to internal APIs for these components: --add-exports to access a strongly encapsulated internal API, --add-opens to access non-public fields and methods by reflection. But remember that accessing the internal JDK APIs is unsafe, and while you can use these options for some time as an emergency, we strongly recommend rewriting the code. 6. Check for deprecated and removed JVM parameters Some JVM parameters were deprecated or removed from the JDK. If your application uses removed parameters, it will print Unrecognized VM option option-name, and the JVM will exit with Error: Could not create the Java Virtual Machine. A warning will be issued in the case of obsolete or deprecated options. The only way of dealing with unrecognized parameters is to remove them. 7. Convert legacy logging flags to a new system GC logging was reimplemented in Java 9 with JEP 271 to align with the unified JVM logging framework. As a result, legacy GC flags won’t be accepted. A guide to mapping legacy garbage collection logging flags to the Xlog configuration can be found in the official documentation’s Convert GC Logging Flags to Xlog section. The document also contains a guide to mapping legacy runtime logging flags to the new configuration. Feeling overwhelmed? BellSoft’s solution to Java migration problem If, after reading this guide, you feel that upgrading the Java version at your company will take much longer than you expected, or you aren’t ready to burden your engineers with migration due to some more urgent tasks, don’t worry — there is a way to stay on Java 8 and still take advantage of Java 17 features and performance! BellSoft prepared a solution that brings the power of JVM 17 to your workloads running on Java 8 — Liberica JDK Performance Edition 8. It couples JVM 17 and JDK 8 and helps enterprises to boost the performance of their applications by up to 10% immediately without changing the version of Java, framework, or libraries. No incompatibility issues, significant code changes, or stress! Learn more about Liberica JDK Performance Edition - [Migration from Java 11 to Java 17](https://bell-sw.com/blog/migration-from-java-11-to-java-17/): The Java platform is getting more powerful with every release. Migration from Java 11 to Java 17 will bring significant improvements in performance, security, and cloud-native capabilities of your Java-based workloads. With modern garbage collectors and new and updated APIs increasing security and facilitating code writing and maintenance, Java 17 is an essential upgrade for projects looking to stay competitive. This guide provides a step-by-step approach to ensure a smooth transition and highlights common pitfalls to avoid. Table of Contents Key reasons to upgrade from Java 11 to Java 17 Upgrade Java version, dependencies, and tools Which Java distribution should you use? Java 11 to Java 17 comparison Common migration issues Removed JVM options Discontinued components Managing removed APIs Strong encapsulation of JDK internals GraalVM and Native Image compilation Recommendations for smooth migration How to stay on Java 11 and get all Java 17 perks? Key reasons to upgrade from Java 11 to Java 17 JDK 11 is a legacy LTS release. It is in maintenance mode meaning that only few improvements are backported to this branch. As a result, the performance of JDK-11 based workloads doesn’t meet the requirements for modern software. But what exactly do we mean by improvements in Java 17? They are: Enhanced Garbage Collection: improved G1 GC, production-ready low-latency collectors ZGC and Shenandoah GC. Better security: Strong encapsulation of JDK internals (JEP 403) forbids using the internal components of the JDK, thus increasing the security of the Java platform and maintainability of the Java applications. New features: Java 17 includes The Foreign Function & Memory API (JEP 412) to interoperate with code and data outside of the Java runtime without the risks of JNI, Context-Specific Deserialization Filters (JEP 415) to configure the deserialization filters via a JVM-wide filter factory, Pattern matching for switch (JEP 406) to express data-oriented queries concisely and safely, Sealed classes (JEP 409) for better control over the code responsible for implementing the classes developers create. Better containerization: Optimizations for running Java in containers, including resource management improvements. Upgrade Java version, dependencies, and tools First of all, change the Java version for your project. Download the binary with the latest release of Java 17 and follow the instructions to complete the installation. You can also use package managers (SDKMAN, Brew, etc.) or Linux repositories to install the JDK on your machine. For instance, to pull Liberica JDK 17 from a Yum repository, run sudo yum update sudo yum install bellsoft-java17 After that, change the JDK version in your IDE and build tools (Maven, Gradle). If you use Docker images, update your Dockerfile as well. For instance, to move your application into a microcontainer with Liberica JDK and Alpaquita Linux, specify: FROM bellsoft/liberica-runtime-container:jre-17-stream-musl Note: you can use a glibc-based Alpaquita version if required for compatibility with the previous Linux distribution. Now, update all tools, libraries, and third-party dependencies to versions that support JDK 17. The framework and its dependencies should also work with version 17. For example, here are the latest versions of common technologies used in Java development that support JDK 17: IntelliJ IDEA 2021.2.1 Eclipse: 2021-09 (4.21) Maven: 3.8.1 Gradle: 7.3 Spring Boot: 2.5 Lombok: 1.18 Naturally, the list goes on, but you get the idea. After all the updates, compile and run your application and run the tests. Solve the remaining issues based on the error messages you see (if any). Some possible problems are described in the sections below. Which Java distribution should you use? The question arises if you find yourself in one of the following situations: You use Oracle Java and are dissatisfied with regular licensing changes or unwilling to pay for costly Java support according to the new Employee for Java SE Universal Subscription; You use Java distributions from several vendors and would like to unify the stack and lower the support expenses; You implement a free JDK build without support and would like to secure your project. Firstly, if you run your workloads in Oracle Java, it is possible to migrate from Oracle to OpenJDK whose developers provide just as performant and secure runtimes. Secondly, you can use one runtime for all your platforms and purposes and receive affordable 24/7 services directly from experienced Java engineers. The key is to select a Java distribution with all features you need and cost-efficient support. Take a look at the Java alternatives overview with a comparison of the most popular Java distributions. Find out about their benefits, features, additional instruments, and supported system configurations, and make an informed decision. Those of you who are looking for a reliable vendor, can download a comparison of commercial offerings provided by OpenJDK vendors, so that you have a clear understanding of the value you get for your money. Java 11 to Java 17 comparison The table below summarizes key differences between Java 11 and Java 17 in terms of performance-specific JVM features and support period. Java 17 Java 11 Garbage Collectors Improved G1 GC, Parallel GC, Serial GC G1 GC, Parallel GC, Serial GC, CMS GC Low-latency Garbage Collectors ZGC, Shenandoah GC* ZGC (Experimental), Shenandoah GC (since 11.0.9) CDS Dynamic AppCDS AppCDS Access to JDK internals Strong encapsulation (JEP 403) Possible with the --illegal-access flag Number of Intrinsics (x86_64) 107 98 End of commercial support for Oracle Java SE 2029 2026 End of free public updates for Oracle Java SE 2022 2019 End of commercial support for Liberica JDK 2030 2032 End of free public updates for Liberica JDK 2030 2032 *Oracle Java doesn't include Shenandoah GC, but it is available with major OpenJDK distributions, including Liberica JDK. For more details on the support terms consult Liberica JDK Support Roadmap. Common migration issues Migration from Java 11 to Java 17 can be associated with a few challenges related to the removed components and APIs and strong encapsulation of JDK internals. Here’s what you should consider. Removed JVM options Several JVM parameters were removed or deprecated up to Java 17. While obsolete or deprecated options make the program produce a warning without affecting its operation, the usage of removed ones will cause the JVM to exit with Error: Could not create the Java Virtual Machine. The solution is to abstain from using the no longer valid parameters. Discontinued components The following tools have been removed from the JDK since Java 11, and if your code depends on them, it will require code modifications or migration to alternative solutions: The Applet API: Applets were removed as no modern browser supports them anymore. If your code uses applets, replace them with modern web technologies such as HTML5 or JavaScript-based solutions. The Security Manager: This API has been marked for removal. Consider adopting alternative sandboxing tools. The Remote Method Invocation (RMI) Activation: The RMI Activation has been removed, so you have to migrate to more robust distributed computing frameworks like gRPC. The Experimental Features AOT and Graal JIT: These features are now part of the standalone open-source GraalVM Project. The Concurrent Mark Sweep (CMS) Garbage Collector: The CMS GC has been deprecated and removed in favor of G1 GC. You can also try out low-latency garbage collectors ZGC or Shenandoah GC. Managing removed APIs There were a few APIs removed from the JDK since version 11. To identify affected areas in your code, utilize the jdeprscan tool to search for usage of deprecated or eliminated APIs. If the scan reveals dependencies on removed APIs, you can: Add related dependencies if the missing modules are now maintained as separate open-source libraries. Rewrite affected code using modern standard APIs. Strong encapsulation of JDK internals One of the JDK 17 features that may affect the migration process is strong encapsulation of JDK internals (JEP 403), whose goal is to make JDK more maintainable and secure. The reflective access to the low-level JDK APIs was restricted in Java 9, but versions 9–16 allowed the developers to use --illegal-access option as a workaround. In Java 17, this parameter is no longer valid and the application will throw the java.lang.reflect.InaccessibleObjectException. To solve the issue, you should: Use standard APIs instead of internal ones. Update libraries to the latest versions that don’t access JDK internals. The jdeps tool with a --jdk-internals option will help you pinpoint problematic libraries by listing class-level dependencies in the JDK internal APIs. As a temporary workaround, you can use the --add-exports and --add-opens options. But remember that using these tools compromises the security of your applications, so resort to them only in case of emergency and for a short term while you are rewriting the code. GraalVM and Native Image compilation If your application relied on the experimental AOT or Graal JIT features in Java 11, consider transitioning to GraalVM. GraalVM is a JDK and JVM written in Java that also offers Ahead-of-time compilation for fast startup times and reduced memory usage. Several distributions of GraalVM are available: Oracle GraalVM and GraalVM Community edition with two downstream versions, Liberica Native Image Kit and Mandrel. Liberica NIK is recommended as a native-image compiler by Spring, so you can use it with your Spring applications to achieve faster startup and peak performance without warmup. Recommendations for smooth migration To ensure a smooth transition from Java 11 to Java 17: Test your application in a special set up testing environment with the updated JDK. Review and update all dependencies and build tools to versions compatible with JDK 17. Resolve any issues that arise after you have started your application. Identify dependence on removed APIs using jdeprscan and rewrite the code accordingly. Identify libraries that depend on internal APIs using jdeps and update them or use alternative solutions. With proper planning, you can efficiently address these challenges, allowing your project to leverage the performance, security, and modern features of Java 17. How to stay on Java 11 and get all Java 17 perks? If you are not ready for a full-fledged migration to Java 17, but want its performance improvements, consider Liberica JDK Performance Edition. This innovative solution that couples JDK 11 and JVM 17 enables you to: Boost performance. Achieve up to a 15% increase in startup times and lower latency. Preserve compatibility. Get the benefits of JVM 17 while maintaining your JDK 11-based infrastructure. Upgrade at your own pace. Upgrade less critical services first, leveraging the advantages of modern Java performance for critical services. - [Liberica JDK 8u382, 11.0.20, 17.0.8, 20.0.2 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u382-11-0-20-17-0-8-20-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 8u381, 11.0.19.0.1, 17.0.7.0.1. CPU releases include patches for Common Vulnerabilities and Exposures (CVE). In addition, we release PSU versions 8u382, 11.0.20, 17.0.8, 20.0.2 with non-critical fixes and general improvements. The release contains 507 fixes and backports overall. BellSoft participated in eliminating 10 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 8 security issues (CVEs) fixed 25 total security fixes in CPU release: in Liberica 8u391: 4 security fixes + 0 in FX; in Liberica 11.0.19.0.1: 9 security fixes + 0 in FX; in Liberica 17.0.7.0.1: 12 security fixes + 0 in FX. In addition, PSU releases include a total of 482 bugs and backports fixed: in Liberica 8u382: 5 security fixes + 37 additional fixes (+ 8 in FX); in Liberica 11.0.20: 10 security fixes + 104 additional fixes (+ 3 in FX); in Liberica 17.0.8: 13 security fixes + 211 additional fixes (+ 3 in FX); in Liberica 20.0.2: 13 security fixes + 69 additional fixes (+ 6 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-22043 5.9 javafx graphics network high none none unchanged none high none CVE-2023-22041 5.1 hotspot compiler local high none none unchanged high none none CVE-2023-25193 3.7 client-libs 2d network high none none unchanged none none low CVE-2023-22044 3.7 hotspot compiler network high none none unchanged low none none CVE-2023-22045 3.7 hotspot compiler network high none none unchanged low none none CVE-2023-22049 3.7 core-libs java.io network high none none unchanged none low none CVE-2023-22036 3.7 core-libs java.util network high none none unchanged none none low CVE-2023-22006 3.1 core-libs java.net network high none required unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 20 CVE-2023-22043 • • • • CVE-2023-22041 • • • CVE-2023-25193 • • • CVE-2023-22044 • • CVE-2023-22045 • • • • CVE-2023-22049 • • • • CVE-2023-22036 • • • CVE-2023-22006 • • • Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [LTS JDK 21 features](https://bell-sw.com/blog/lts-jdk-21-features/): A new LTS version has always been the big news in the Java world, and JDK 21 is no exception. It was moved to Rampdown Phase One on June 16 meaning that the feature set has been frozen. An extensive set of 15 JEPs includes new, enhanced, and finalized functionality. Let’s take a dive into the upcoming LTS release — it might be just the version you will stick to for the years ahead! Table of Contents Novelties JEP 430: String Templates (Preview) JEP 431: Sequenced Collections JEP 443: Unnamed Patterns and Variables (Preview) JEP 445: Unnamed Classes and Instance Main Methods (Preview) JEP 451: Prepare to Disallow the Dynamic Loading of Agents JEP 452: Key Encapsulation Mechanism API Finalized features JEP 440: Record Patterns JEP 441: Pattern Matching for switch JEP 444: Virtual Threads Improved features JEP 439: Generational ZGC JEP 442: Foreign Function & Memory API (Third Preview) JEP 446: Scoped Values (Preview) JEP 448: Vector API (Sixth Incubator) JEP 453: Structured Concurrency (Preview) Deprecated functionality JEP 449: Deprecate the Windows 32-bit x86 Port for Removal To upgrade or stay put? Novelties JEP 430: String Templates (Preview) String templates are a preview language feature and API aimed at facilitating the expression of strings that contain values computed at run time. The existing Java mechanisms of concatenating literal text and expressions produce hard-to-read code or are associated with verbosity. Other languages utilize string interpolation, which allows for conciseness but brings potential security risks. Template expressions help to achieve clarity of interpolation without introducing security vulnerabilities. Take a look at the code snippet with a template expression on the second line: String name = "Joan"; String info = STR."My name is \{name}"; assert info.equals("My name is Joan"); // true The expression consists of a template processor STR, a dot, and a template with an embedded expression \{name}. Embedded expressions can be strings, perform arithmetic operations, invoke methods, access fields, and even spread over multiple lines. Template processors can use only the values in the embedded expressions, and execute at run time only. In addition, it is impossible to use the templates without the template processor responsible for safe interpolation and validation of a result, which increases safety of operations. JEP 431: Sequenced Collections Sequenced collections introduces collections with a defined encounter order and uniform APIs for accessing the first and last elements, and processing the elements in forward and reverse order. Three new interfaces — sequenced collections, sets, and maps — will be retrofitted into the existing collections type hierarchy. All three interfaces have new methods facilitating the development process: A sequenced collection has a reversed() method to view the collection in reversed order, process the elements in both directions, and perform all the usual iteration operations such as forEach(), stream(), etc. A sequenced set includes addFirst(E) and addLast(E) methods that can move the elements in the appropriate position if it is already present in the set. A sequenced map has the put*(K, V) methods whose functionality is similar to add*(E) methods of sequenced sets. JEP 443: Unnamed Patterns and Variables (Preview) The unnamed patterns and variables will improve the readability and maintainability of code by Eliding the unnecessary type and name of a record component in pattern matching and Identifying variables that must be declared but will not be used. Both are denoted by the underscore character _ . Unnamed patterns enable the developers to omit the components in record patterns that are not used, for instance: ... instanceof Point(int x, _) case Point(int x, _) Unnamed variables substitute the names of variables, which are not used (for example, in try-with-resources or try-catch blocks), e.g.: int _ = q.remove(); ... } catch (NumberFormatException _) { ... (int x, int _) -> x + x JEP 445: Unnamed Classes and Instance Main Methods (Preview) Students embarking on a Java development journey may find some enterprise-level features too difficult. The new feature is aimed at giving them the opportunity to write single-class programs and gradually expand them as their knowledge grows. It will also be useful for experienced developers who want to write simple, concise applications without programming-in-the-large composition of program components (i.e., when enterprise-level features interact with each other through well-defined protocols, but hide internal implementation details). For instance, the basic HelloWorld program contains several features, which are hard to comprehend, but unnecessary for novices: public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } It can be simplified to: class HelloWorld { void main() { System.out.println("Hello, World!"); } } This program can be made more complex with time as students learn necessary concepts. But at the same time, there's no need to introduce a simplified Java dialect for educational purposes. JEP 451: Prepare to Disallow the Dynamic Loading of Agents In JDK 21, the users will receive warnings when agents are loaded dynamically into a running JVM. JEP 451 lays ground for a future release that disallows the dynamic loading of agents by default in line with the ongoing process of enhancing Java integrity. Agents are components that can alter the code when the application is running. They are commonly used by serviceability tools such as profilers and troubleshooting instruments, but the developer must grant approval to alter the application. However, some libraries that use agents can bypass this requirement and attach to the running JVM silently, thus increasing the security risks. The proposal is to make the user explicitly allow the dynamic loading with the -XX:+EnableDynamicAgentLoading option on the command line. Luckily, most serviceability tools do not use dynamic agent loading and therefore, will not be affected. As for the libraries, they must load the agent at startup with the -javaagent/-agentlib options: the maintainers are encouraged to update their documentation with the explanation of how to load agents at startup. JEP 452: Key Encapsulation Mechanism API The Key Encapsulation Mechanism (KEM) API introduces an encryption technique for securing symmetric keys with asymmetric (public key) cryptography. KEM uses public key properties to derive a related symmetric key without padding. Right now, Java doesn’t have a standard KEM API. However, it is an important modern technique for defending against cyberattacks and will likely be part of the next generation of standard public key cryptography algorithms. Finalized features These features were introduced in previous releases and after a series of improvements and follow-up changes have taken a final form in this LTS version. JEP 440: Record Patterns Record patterns, which are used to deconstruct record values to improve pattern matching, were first introduced in JDK 19. JEP 440 finalizes the feature with several enhancements based on the feedback. The most significant change is the removal of support for record patterns appearing in the header of an enhanced for statement. JEP 441: Pattern Matching for switch Pattern matching for switch expressions and statements was proposed in JDK 17 and refined in the following releases. The aim of the functionality is to enhance the expressiveness, applicability, and safety of switch statements. With pattern matching for switch, developers can test the expressions against specific patterns, thus making complex data queries more concise and reliable. The finalized feature includes the following improvements: The removal of parenthesized patterns; Allowing for qualified enum constants as case constants in switch. JEP 444: Virtual Threads Virtual threads enhance the concurrent programming in Java by providing a mechanism to create thousands of lightweight threads depending on the tasks at hand, which can be monitored, managed, and debugged like the usual platform threads. You can read more about this functionality in the dedicated article. Virtual threads were included as a preview API in JDK 19. JEP 444 finalizes the feature and includes a few improvements based on the feedback: Always support thread-local variables belonging to the ThreadLocal API that enables the developers to store data accessible for a specific thread only; Virtual threads created directly with the Thread.Builder API are now monitored during their lifetime by default. They can also be observed via the new thread dump, which will group plentiful virtual threads in a meaningful way. Improved features JEP 439: Generational ZGC ZGC is a scalable low-latency garbage collector that has consistently low pause times (measured in microseconds) regardless of the heap size. However, the current non-generational ZGC stores young and old objects together and has to collect all objects every time it operates. As most young objects die young, and old objects tend to stick around, collecting young objects requires fewer resources and yields more memory. Therefore, a Generational ZGC will maintain young and old objects separately and collect young objects more frequently, thus reducing the GC CPU overhead and heap memory overhead. JEP 442: Foreign Function & Memory API (Third Preview) Foreign function & memory API enables Java applications to safely interact with code and data outside of the Java runtime. The FFM API is aimed at replacing the Java Native Interface with a more reliable, pure-Java development model. This is the third preview of the FFM API with the following amendments: Centralized management of the lifecycle of native segments through the Arena interface; Enhanced layout paths with a new element to dereference address layouts; A new linker option to optimize calls to short-lived functions that will not upcall to Java; A new fallback native linker implementation, based on libffi, to facilitate porting; Removed VaList class. JEP 446: Scoped Values (Preview) Scoped values enable the developers to share immutable data within and across threads with the aim of more reliable and manageable data management in concurrent applications. Scoped values should be preferred to thread-local variables, because, unlike them, scoped values are immutable and are associated with smaller footprint and complexity. This feature was incubated in JDK 20 and is now a preview API. JEP 448: Vector API (Sixth Incubator) Vector API increases the performance of vector computations that compile reliably at run time to optimal vector instructions. The feature was first introduced in JDK 16. You can find out more about Vector API performance in an article dedicated to Java 17 features. This is a sixth incubator with the following notable enhancements apart from bug fixes: Addition of the exclusive or (xor) operation to vector masks. Improved performance of vector shuffles, especially when used to rearrange the elements of a vector and when converting between vectors. JEP 453: Structured Concurrency (Preview) Structured concurrency enables the reliable coordination of virtual threads and improves observability and maintainability of concurrent code. The feature was included into JDK 19 as an incubating API and reincubated in subsequent releases. This is a preview API with one notable change: the StructuredTaskScope::fork(...) method returns a [Subtask] instead of a Future as before. The Future is more useful when multiple tasks are treated as individual tasks, and not as a single unit of work (which is the goal of structured concurrency). The Future involves calling a get() method, which blocks until a result is available. This is counterproductive in the case of StructuredTaskScope, which will now use a resultNow() that never blocks. Deprecated functionality JEP 449: Deprecate the Windows 32-bit x86 Port for Removal The last Windows OS that supports 32-bit operation (Windows 10) will reach end of life in October 2025. At the same time, the usage of virtual threads on Windows x86-32 doesn’t bring the expected benefits. Therefore, the Windows 32-bit x86 port becomes redundant and will be removed in a future release. To upgrade or stay put? Early-access builds are already available. If you are going to upgrade to the new LTS version, you can test the new functionality and start planning the migration strategy. BellSoft will support JDK 21 until March 2032, so consider making this version a solid ground for your project for the coming years. But at the same time, we understand how complicated and time-consuming the migration process might be, and the further you are from the current version, the more issues you will have to deal with upon upgrading. What if you could bring the performance of newer versions to your JDK 11-based enterprise application without switching the Java version? BellSoft has the solution for you – no significant adjustment, no code rewriting, no compatibility problems, but instant performance boost! Meet Liberica JDK Performance Edition, which couples JVM 17 and JDK 11 and improves the startup, throughput, and latency of application even at the default settings. Click on the button below to learn more about its capabilities and features. Discover Liberica JDK Performance Edition - [Red Hat limits access to RHEL source code](https://bell-sw.com/blog/red-hat-limits-access-to-rhel-source-code/): June 2023 was marked by big news in enterprise software development: the source code of Red Hat Enterprise Linux (RHEL) ceased to be freely available. Red Hat announced that “CentOS Stream will now be the sole repository for public RHEL-related source code releases. For Red Hat customers and partners, source code will remain available via the Red Hat Customer Portal.” This move will have a far-reaching impact on numerous enterprises and individual developers who use free RHEL-based distributions or Red Hat base images. A shot out of the blue? Indeed, the Big Blue The decision to put RHEL source code behind a paywall was ruthless but not entirely unexpected. From the beginning, RHEL has been a Linux distribution aimed at enterprises, but the solution has always been open source. The source code was freely available to everyone, customers and non-customers alike. In 2003, Red Hat decided to chip a community version off its enterprise solution, and that’s how Fedora Linux came to be – an upstream version of RHEL. Fedora constantly pulls new features and fixes from upstream Linux projects and rolls out a new version every six months. Red Hat periodically takes a Fedora release and builds a stable production-ready RHEL version on top of that. But at the same time, several other free projects bloomed on RHEL source code that provided full compatibility with the official distribution. Red Hat was obviously dissatisfied that some developers preferred using a free version of their Linux. So in 2014, Red Hat acquired CentOS, a community project that became the most popular free RHEL-based distro, and efficiently killed it in 2020 with the pretext of focusing on CentOS Stream, a development branch of RHEL. Curiously, IBM completed the acquisition of Red Hat a year before, in 2019. Although CentOS was discontinued, other projects such as AlmaLinux, Rocky Linux, Oracle Enterprise Linux (OEL) continued building free binaries based on RHEL source code. This certainly set Big Blue’s teeth on edge. Judging by how IBM is used to doing its business, a blow to these projects was only a matter of time. The motivation behind closing the source code of RHEL is “The engineering levels of investment and the new priorities we’re addressing for customers and partners now make maintaining separate, redundant repositories inefficient.” The wording doesn’t sound credible because keeping public repositories and offering optional commercial services is the common practice in the OSS world, and Red Hat had managed to keep up with it before IBM came into play. In reality, IBM could be looking for ways to increase revenue by leaving developers with no alternative but to pay for RHEL subscription. After all, finding a suitable substitution may take time, and meanwhile, the OS version in use rots away, eaten by vulnerabilities. RHEL’s source code is still available to IBM Red Hat customers, but the license prohibits its redistribution. As CentOS Stream is an unstable development/preview version of RHEL, it is impossible for developers using this distribution to reach bug-to-bug compatibility with RHEL, which makes it as good as useless for enterprise use. What do you do now? If you are a Red Hat customer, nothing changes for you, at least for now. Keep in mind, though: today, the company cuts the developers off their product’s source code, and tomorrow, it changes licensing conditions drastically. Unfortunately, such incidents are not unique or limited to Linux. Developers who used the AlmaLinux, Rocky Linux, or OEL distribution should be concerned because it is currently unclear how these distributions will continue to evolve. But the most alarming question is: do UBI images used for building small containers become closed-source, too? If not, how long will it take IBM to bottle them up? After all, UBI images are utilized across the board (even despite the fact that developers can’t simply add necessary OS packages to UBI because of discrepancies with the RHEL license), presenting a lucrative field for the ever-hungry Big Blue. Broken trust of the community is the worst aftermath For IBM, taking such a course is business as usual. But the community around the corporation, individual developers and ISVs alike, got a clear signal that they shouldn’t rely on assumptions: everything that wasn’t legally guaranteed can be taken away any time even if a prominent organization is involved. BellSoft has been committed to freedom since the beginning. The source code of our products is available to everyone, and companies who chose our support services have never experienced sudden changes to the licensing agreement. We offer Alpaquita Containers based on open-source Alpaquita Linux and Liberica JDK. Use them to build secure and performant microcontainers free of charge, or take advantage of our support services — it’s your call! Try Alpaquita Containers - [Liberica Native Image Kit 23.0.1 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-1-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 23.0.1 for JDK 17.0.8 and 20.0.2 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Summary of fixes and enhancements Notable changes: ParallelGC is now available on Windows. The feature is aimed at enhancing the GC performance in GraalVM CE by reducing GC pause times. It is currently experimental and is not suitable for production use. However, we encourage you to test it out and report any discovered bugs via our GitHub page. Fixed compilation of JavaFX FXML applications. List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-22043 5.9 javafx graphics network high none none unchanged none high none CVE-2023-22041 5.1 hotspot compiler local high none none unchanged high none none CVE-2023-25193 3.7 client-libs 2d network high none none unchanged none none low CVE-2023-22044 3.7 hotspot compiler network high none none unchanged low none none CVE-2023-22045 3.7 hotspot compiler network high none none unchanged low none none CVE-2023-22049 3.7 core-libs java.io network high none none unchanged none low none CVE-2023-22036 3.7 core-libs java.util network high none none unchanged none none low CVE-2023-22006 3.1 core-libs java.net network high none required unchanged none low none Conclusion BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [BellSoft brings the performance of JVM 17 to JDK 11 with Liberica JDK Performance Edition](https://bell-sw.com/blog/bellsoft-brings-the-performance-of-jvm-17-to-jdk-11-with-liberica-jdk-performance-edition/): We are happy to announce the release of Liberica JDK Performance Edition, which brings the performance of JVM 17 to enterprise workloads running on JDK 11! With Liberica JDK Performance Edition, there’s no need to worry about migration to newer JDK versions: the companies will notice the instant performance boost up to 10–15% with little to no code adjustments. JVM 17 to JDK 11 coupling Every JDK release contains new features and improvements aimed at making Java applications faster and more resilient, which is especially relevant for cloud-native applications, where latency and throughput directly affect user experience. But as JDK 11 is in the maintenance mode, less and less changes are ported to this version from new releases. However, not all enterprises are willing or have the capacities to migrate to the latest JDK versions – some operate complex applications, some use legacy libraries and APIs, and others don’t have enough developers to perform the migration. As a result, these companies miss on the opportunities to increase the performance of their programs. So what can IT teams do to increase the performance of their JDK 11-based projects without changing the code? BellSoft offers them a solution — Liberica JDK Performance Edition! Liberica JDK Performance Edition (or liberica-perf for short) is an enhanced version of Liberica JDK that couples the JVM 17 and JDK 11, bringing the efficiency of the latest JVM versions to established workloads. Who will benefit from Liberica JDK Performance Edition? Companies that have a complex development environment based on JDK 11 with an established set of libraries will benefit from new features and improvements (discussed in detail below) without the need to rewrite the application code. Most enterprise applications will run on Liberica JDK Performance Edition without change. But the developers will notice an immediate improvement in startup, latency, and throughput thanks to fixes and additional capabilities accumulated up to JDK 17. Essential enhancements Improved garbage collection: Enhanced low-latency Shenandoah GC for large heaps; Production-ready ZGC with fixes and augmented properties: concurrent relocation, consistent throughput on a large heap, etc.; G1GC with new capabilities such as concurrent cleanup, parallel FullGC, improved heap sizing algorithm, etc. In addition, JEP 348 enables G1GC to return committed heap memory to the OS when idle, thus helping companies optimize resource consumption in specific scenarios; Runtime improvements: Enhanced NUMA support to improve the performance of large enterprise applications that run a single JVM over multi-socket machines; More convenient logging with the improved unified JVM logging framework; Improved native memory tracking to optimize memory footprint; Same workflow you are used to — better results Liberica JDK Performance Edition can be used for developing and running Java applications on Linux-based headless or GUI systems. The installation process is as simple as with any JDK binary. Even with default settings, Liberica JDK Performance Edition will deliver a notable improvement in startup time, latency, and throughput — check out the benchmarking results on the product page. Learn more about Liberica JDK Performance Edition Most workloads won’t require significant re-configuration but will receive the benefits described above. The changes are related to some tools and libraries that are not supported in JDK 17 or behave differently. In addition, some VM options were removed or added in line with JVM 17 specifications. A full list of changes to VM parameters and tools compatibility can be found in the User’s Guide. Go to Liberica JDK Performance Edition User’s Guide BellSoft’s engineers will help to perform necessary adjustments to your project if required for smooth integration of Liberica JDK Performance Edition. More power under the hood without effort from your part Are you ready to benefit from JVM 17 capabilities without upgrading the JDK version? Liberica JDK Performance Edition is a one-click solution to the migration problem! The solution is available starting August 1st. Customers who already have a Liberica JDK Subscription receive Liberica JDK Performance Edition for free together with other Java utilities. If you would like to know more about the services we provide with Liberica JDK, contact us, and we will be happy to answer all your questions. Contact us - [What is a software bill of materials (SBOM)](https://bell-sw.com/blog/what-is-a-software-bill-of-materials-sbom/): According to Gartner’s prediction, 45% of enterprises worldwide will have faced an attack on their software supply chain by 2025. In most cases, hackers target unpatched vulnerabilities, which can be plentiful, considering that an ordinary enterprise project usually uses hundreds of dependencies. A software bill of materials (SBOM) is your first line of defense in this scenario. It doesn’t substitute other security mechanisms but enables you to inspect every nook and cranny of your IT infrastructure most conveniently. This article examines what an SBOM is and what risks you run without it. It also delves into SBOM contents and formats so you clearly understand the data you need to provide or require from your suppliers. Table of Contents What is an SBOM? Why do organizations need an SBOM? What data an SBOM contains How to generate an SBOM Overview of the CycloneDX format A software bill of materials is not a luxury, but a must! What is an SBOM? In short, a software bill of materials is an inventory of all dependencies used to build a software product, which is similar to a traditional bill of materials listing all raw materials, assemblies, and components utilized for product manufacture. A more detailed definition by the National Telecommunications and Information Administration (NTIA) states “A software bill of materials (SBOM) is a complete, formally structured list of components, libraries, and modules that are required to build (i.e., compile and link) a given piece of software and the supply chain relationships between them.” Note that this list includes open-source and proprietary software regardless of whether it is freely available or has restricted access to customers only. However, an SBOM doesn’t imply disclosure of source code: the code may remain confidential at the vendor’s discretion. To continue the comparison with a BOM: giving a list of materials doesn’t mean revealing the manufacturing processes. Software suppliers or developers generate an SBOM for their software product and update it with every release. If a vulnerability is found in a component, an SBOM allows for prompt identification of affected software and remediation. Why do organizations need an SBOM? The purpose of an SBOM depends on your role. The developers can control the integrity of dependencies in their project, the purchasers can make an informed decision about the product, and software operators can monitor vulnerabilities and react promptly. But in any scenario, an SBOM helps you to face the following challenges of the modern digitalized world: Prevent hidden vulnerabilities. About 90% of companies utilize open-source software, and according to Snyk, an average project contains about 49 vulnerabilities in direct and indirect dependencies. Don’t take it as if the issue concerned only OSS, though. Closed-source software also has vulnerabilities, but the data is not made public (which doesn’t prevent hackers from exploiting them). An SBOM increases software transparency — the whole project structure is laid open to you, and developers can immediately see if there are components with vulnerabilities. As a result, the developers can promptly assess the risks and update the software version or take other mitigating actions to protect the system from attacks until the patch is ready. Minimize licensing risks. Any third-party code we use in development is licensed. Developers often pull dependencies from public repositories without reading the licensing conditions, which may lead to lawsuits or other unpleasant consequences. For instance, using a GPL-licensed code without the CPE will force you to open your project’s source code, even if it is meant to be proprietary. An SBOM contains information about component licensing, enabling developers to avoid legal risks and protect intellectual property. Gain perspective on the state of software components. An enterprise project sometimes makes it hard to keep track of all component versions. As a result, a project may contain outdated components or even libraries that are insufficiently supported or even abandoned. Consequently, security and maintainability risks increase because fixes and patches are late or don’t come at all. An SBOM provides data about component versions, so you can determine whether you use a fresh or outdated dependency. Promote vendor accountability. Suppliers of open-source and proprietary software must provide an SBOM with their product, which proves that they ensure the security, licensing integrity, and dependency health of their product. Therefore, if you are a software vendor, an SBOM will give you a competitive edge over those who don’t bother with it. If you are a consumer/purchaser, imagine that software without SBOM is like a food product without a list of ingredients. Ensure legislative compliance. The U.S. President’s Executive Order No. 14028 on Improving the Nation’s Cybersecurity dictates that U.S. state agencies must use only software supplied with a software bill of materials. Judging by the growing global concern about cyber threats, this requirement will likely be extended to a broader range of enterprises. What data an SBOM contains So what kind of information must an SBOM include? According to the NTIA’s Minimum Elements For a Software Bill of Materials pursuant to the EO 14028, an SBOM must contain the following: Data fields include basic information about software components that allows for identification of these components across the software supply chain and mapping to other data sources such as vulnerability databases: Supplier name, Component name, Component version, Other unique identifiers (used to identify a component or serve as a look-up key for databases), Dependency relationship (reflects the transitivity between software and a component or a sub-component), Author of SBOM data, Timestamp (date and time of SBOM generation). Automation support implies that an SBOM is generated in a commonly used and machine-readable format, which enables companies to integrate SBOMs into their vulnerability management practices, perform auditing, or take advantage of SBOM data in any other way. Practices and processes imply that an SBOM is not just a data set generated once and for the product lifecycle. For smooth integration of an SBOM into the corporate SDL (secure development lifecycle) processes, certain elements must be addressed in an agreement to provide an SBOM: Frequency: a new SBOM must be created with every software release or update. Depth: an SBOM must contain all primary components with transitive dependencies. A customer can specify the depth of transitive steps. Known unknowns: if an SBOM doesn’t contain the whole dependency graph, a supplier must specify “known unknowns,” when the presence of dependencies for a component is unknown and incomplete. Distribution and delivery: an SBOM must be delivered in a timely manner and have all relevant access permissions and roles. Access control: the software supplier and customer should determine through contracts, licensing, or other legal mechanisms whether an SBOM can be made public or should remain confidential. Accommodation of mistakes: as the mechanisms of software supply chain security are still evolving, the occasional errors in SBOM implementation should be tolerated, which will facilitate the continuous improvement of the tool. As the document name suggests, these data elements represent a required minimum and may be extended with time as the cybersecurity standards progress. In addition, enterprises may demand additional information as deemed necessary. How to generate an SBOM It is possible to create an SBOM manually, but it is a time-consuming and meticulous task. Fortunately, numerous tools are present;y available to aid developers in generating the document automatically, whether during or post development: FOSSA, Codenotary, Anchor, to name a few. Some solutions scan the open-source dependencies, and others can also analyze proprietary libraries. In addition, you can use a software composition analysis (SCA) tool such as JFrog, Snyk, Spectral, etc., which identifies and gathers information about open-source components that can be further used for building an SBOM. Another question is — how to draw it up correctly? As mentioned above, SBOMs must be in a commonly accepted, universal form for smooth integration into corporate security processes. As such, the SBOM must be generated in one of these standard industry formats: Software Package Data eXchange (SPDX) by the Linux Foundation, CycloneDX by the Open Worldwide Application Security Project (OWASP) Foundation, Software Identification (SWID) tags defined by the ISO/IEC 19770-2:2015 standard. Let’s dig deeper into one of these specifications, CycloneDX. Overview of the CycloneDX format CycloneDX can be used for a wide variety of use cases, including but not limited to software bill of materials, operations bill of materials, vulnerability exploitability exchange, SBOM for low code application platforms, and so on. CycloneDX files can be generated in JSON, XML, or Protocol Buffers (protobuf). Every file has a unique serial number conforming to RFC-4122, and the version is incremented by ‘1’ each time a new file is generated. The file contents include eight root-level elements summarized in the table below. CycloneDX element Contents Metadata Supplier, manufacturer, tools used for SBOM creation or authors in case of manual generation, target component, licensing information for SBOM itself Components Complete inventory of first- and third-party components: manufacturer, licensing, pedigree, provenance, etc. Services External APIs that may be called, with endpoints, trust zone, and data flow Dependencies Dependency graph with both direct and transitive dependencies Compositions Constituent parts and their completeness Vulnerabilities Known and previously unknown vulnerabilities with exploitability, risk ratings, advisories, etc. Formulation Description of manufacture and deployment processes: formulas, tasks, workflows and so on Annotations Additional textual information in the form of comments, notes, etc. Multiple extension points are also available for prototyping new functionality and supporting special use cases. To generate a CycloneDX file, you can use one of the tools supporting the format. If you utilize Cloud Native Buildpacks for containerization, the platform enables you to generate CycloneDX-based SBOMs for application images. And Maven offers a plugin for drawing up SBOMs in CycloneDX with information about direct and transitive project dependencies. A software bill of materials is not a luxury, but a must! We hope we have persuaded you that a software bill of materials is indispensable for securing a software supply chain. As a developer, you must be sure of your project’s integrity. As a customer, you have the right to demand an SBOM from your software vendor. Some regulations already demand that software vendors provide a software bill of materials. We will discuss the topic in detail in our second article dedicated to SBOMs. BellSoft supplies its customers with software BOMs for Liberica JDK so enterprises have a transparent and secure Java development environment. Contact us to learn more about the service. Contact us - [U.S. and EU regulations are demanding a software bill of materials (SBOM)](https://bell-sw.com/blog/u-s-and-eu-regulations-are-demanding-a-software-bill-of-materials-sbom/): A software bill of materials is an indispensable mechanism for securing a software supply chain, as we found out in our previous article. So much so that governments explicitly demand the SBOM adoption via legislative acts that sprouted across the globe over the past few years in answer to growing cybersecurity threats. This article provides an overview of the U.S. and EU regulatory landscape concerning SBOM adoption for cybersecurity fortification. It doesn’t give legal advice and only provides a general overview of the topic. Table of Contents U.S. Legislation Executive Order 14028 on Improving the Nation’s Cybersecurity DHS Software Supply Chain Risk Management Act FDA medical device cybersecurity requirements European Union cybersecurity requirements The EU Cyber Resilience Act (CRA) Recommendations U.S. Legislation Executive Order 14028 on Improving the Nation’s Cybersecurity One of the most well-known U.S. legislative documents concerning cybersecurity — Executive Order on Improving the Nation’s Cybersecurity — was released in May 2021 in response to the alarming increase of cyberattacks on software supply chains. The most devastating and far-reaching was the SolarWinds attack that affected over 18,000 customers and hundreds of organizations worldwide, including the U.S. federal agencies. The Executive Order describes various measures to secure software systems utilized by federal agencies. Therefore, it relates to informational or operational technology service providers contracted by U.S. governmental bodies. One of the cybersecurity goals set forth is to increase the transparency of software supply chains. This will raise awareness of present vulnerabilities and help IT teams react promptly to their appearance. For this purpose, service providers must Secure software development environments by applying multi-factor authentication (MFA), encrypting data, documenting and minimizing dependencies, etc.; Employ tools or processes to maintain trusted source code supply chains; Implement automated tools to scan for and remediate known vulnerabilities; Maintain precise and up-to-date data on software components’ origin; Provide a software bill of materials for each product; Follow the secure software development practices; Ensure and attest to the origin of utilized open-source software to the extent practicable. As you can see, many of these measures are focused on the obligation to collect and maintain the data on known vulnerabilities and the origin and integrity of software components used in the development process and included in finished products. Although the document concerns only vendors working with the U.S. government, the practices described above are helpful for all ISVs because they help to continuously monitor the security of software during development and after deployment, implement patches on time, steer clear of libraries of unknown or unverified origins, and thus minimize the risks of cyberattacks. DHS Software Supply Chain Risk Management Act The DHS Software Supply Chain Risk Management Act of 2021 obliges contractors to the Department of Homeland Security to provide a software bill of materials with their IT products or services. In addition, they must provide the certificate that each item in the SBOM is free of known vulnerabilities, a list of all vulnerabilities present in the technology, and a plan to mitigate or reserve them. FDA medical device cybersecurity requirements The U.S. Food and Drug Administration (FDA) raised concerns about the insufficient security of medical devices in response to the increased number of cyberattacks on healthcare institutions. The agency was pushing for legal cybersecurity requirements for medical device manufacturers, including the provision of an SBOM. As a result, the Consolidated Appropriations Act of 2023, signed on December 29, 2022, includes Section 3305, which gives the FDA the authority to regulate the cybersecurity of medical devices. The Act amends the Federal Food, Drug, and Cosmetic Act that now has a 524B section, “Ensuring Cybersecurity of Devices,” which forces the applicants submitting an application to the FDA for a medical device containing software that connects to the internet to Submit a plan for monitoring and addressing postmarket cybersecurity vulnerabilities; Design and maintain processes for ensuring medical device cybersecurity, including timely postmarket integration of patches for known vulnerabilities; Provide a software bill of materials that includes data on commercial, open-source, and off-the-shelf software components; Comply with other requirements the FDA may put forward. These cybersecurity requirements are applicable to applications submitted starting March 29, 2023. However, if the device was previously authorized, but the manufacturer introduced changes that require a new premarket review, then the requirements apply to the new submission. The FDA will cooperate with organizations that do not have outlined plans for the remediation of discovered vulnerabilities to help them improve their cybersecurity documentations. But starting October 1, 2023, the FDA will not accept submissions from companies without a remediation plan. BellSoft provides a software bill of materials with Liberica JDK to help companies developing Java applications comply with the regulations. Contact us to learn more about the service. Contact us European Union cybersecurity requirements Although some European countries such as Finland and Germany have already introduced specific cybersecurity requirements on the legislative level, the main document applicable to all Member States is the Cyber Resilience Act (CRA) proposed by the European Commission. The EU Cyber Resilience Act (CRA) The EU Cyber Resilience Act, proposed on September 15, 2022, is the first EU-wide legislation addressing cybersecurity requirements for software and hardware manufacturers and developers with digital elements connecting to the internet. In contrast to the U.S. Executive Order, the CRA extends to all vendors who place their products on the EU market. The European Commission sees two main reasons for a growing number of cyberattacks: the spread of vulnerabilities coupled with the lack of timely updates and insufficient access to the information by users, preventing them from choosing more secure products. Consequently, two key goals of the CRA are: Establish the conditions for the development of products with fewer vulnerabilities and ensure that vendors make security a priority throughout the whole product life cycle; Raise awareness among users about the importance of cybersecurity, helping them to select and use sufficiently secure products with digital elements. To achieve these goals, the European Commission sets forth essential requirements for manufacturers of digital products, who should among other things Ensure that their products are delivered without known vulnerabilities; Notify the European Union Agency for Cybersecurity (ENISA) about known exploited vulnerabilities and any cybersecurity incidents affecting the security of their products; Notify the maintainers of software components, including open source ones, on discovered vulnerabilities; Implement vulnerability disclosure policies to facilitate vulnerability reporting; Identify and document the components of a digital product, including by drawing a software bill of materials. The CRA stresses the importance of an SBOM in the following statement (Article 37): “A software bill of materials can provide those who manufacture, purchase, and operate software with information that enhances their understanding of the supply chain, which has multiple benefits, most notably it helps manufacturers and users to track known newly emerged vulnerabilities and risks. It is of particular importance for manufacturers to ensure that their products do not contain vulnerable components developed by third parties.” The document includes additional requirements to the SBOM generation. Firstly, it should cover, at the very least, the top-level dependencies of the product in a commonly used and machine-readable format (Section Two, Annex I). Secondly, the European Commission may specify the format and SBOM elements, as well as the additional information, format, and procedures pertaining to notifications on vulnerabilities and incidents (Section 63). The non-compliance with the CRA requirements is subject to fines up to 15 000 000 EUR (Article 53). Although the CRA has not been passed yet and is therefore subject to amendments, two things are clear: Manufacturers are obliged to gather information about software components they use in development, including data on vulnerabilities, and As soon as the CRA gets adopted, it will significantly impact all software vendors working on the EU market. Recommendations The rapid increase of legislations and initiatives promoting vulnerability identification and tracking shows the worldwide tendency towards greater transparency of software supply chains. And although drawing up an SBOM is not yet legally binding on all software vendors, the situation will likely change. What should you do if you are already among vendors obliged to follow the regulations? The first step is to draw up a plan on managing the vulnerabilities and exploits, The second step is to develop and implement the process of vulnerability management based on the plan, Finally, generate an SBOM in one of the recognized formats. BellSoft can assist you at the planning stage if you want to migrate to reliable and secure JDK and Linux distributions with clear licensing conditions and LTS support from one vendor, delivered as part of Alpaquita Cloud Native Platform. Alpaquita Cloud Native Platform will become a part of your processes as a comprehensive solution for Java applications, developed in line with the Secure Development Lifecycle (SDL) practices and receiving regular security patches guaranteeing that both Linux and Java are free of known vulnerabilities. We provide our own supply channels for builds, binaries, and metadata, as well as a software bill of materials, helping you to keep your Java infrastructure secure and comply with the regulations. Contact us to learn more about the services. Contact us - [BellSoft releases Alpaquita Containers for Spring Boot applications](https://bell-sw.com/blog/bellsoft-releases-alpaquita-containers-for-spring-boot-applications/): Small containers with Spring Boot applications save cloud resources and facilitate developers’ life as we found out in our previous article. Keeping that in mind, BellSoft engineers continue working on technologies that make Java containers faster, smaller, and safer. Their endeavors resulted in Alpaquita Containers for the Spring Boot applications, which are now generally available for download. The power of optimized Linux and Java under the hood Alpaquita Containers are based on Alpaquita Linux and Liberica JDK Lite. Both technologies are aimed at improving the performance and decreasing memory footprint of Java applications. Alpaquita Linux is a lightweight Linux distributions inspired by Alpine, but boasting multiple optimizations for enhanced security and flexibility, such as: Three libc variants (default musl, optimized musl, and glibc) and four mallocs; The base image size is only 3.28 MB (musl) and 8.76 MB (glibc); Kernel and userspace security hardening; LTS version for enterprise use. Liberica JDK is the default Java runtime in Spring Boot. Liberica JDK Lite is the flavor optimized for cloud deployment, which delivers reduced static footprint thanks to better compression for modules and improvements backported from newer JDK versions. You can read more about Liberica JDK Lite features in the overview of Liberica JDK flavors. Benefits of Alpaquita Containers Here is what Alpaquita Containers help Spring Boot developers to achieve: Cost optimization. Smaller container images consume less cloud resources, thus leading to reduced cloud bills; Accelerated development. Small containers take less time to load, which is crucial in the case of numerous push/pulls or slow internet connection; Security. Both Linux and JDK receive regular updates keeping the free of known vulnerabilities. Support with 24/7 service and emergency patches is also available. The performance studies comparing an image based on Alpaquita Linux and Liberica Lite vs the official OpenJDK image on Docker hub (which is not supported anymore but can be used for reference as an “average” OpenJDK implementation) showed the 30 % improvement in image size and RAM consumption associated with the Alpaquita image in comparison to the OpenJDK image. Alpaquita container performance study Try Alpaquita Containers now! Alpaquita Containers are free to use — try them out with your application and let us know about the optimizations you managed to achieve! Head over to Alpaquita Containers page and explore the detailed guide on building the container and running the tests to measure its performance. - [BellSoft releases Alpaquita Containers for Spring Boot](https://bell-sw.com/news/bellsoft-releases-alpaquita-containers-for-spring-boot/): SAN JOSE, Calif., Aug. 22, 2023 /PRNewswire/ -- BellSoft continues to improve the microservice architecture of Java applications and transform Java to cloud-native. We are glad to introduce Alpaquita containers for the Spring Boot developers' community. The software is now generally available for download. What are Alpaquita Containers? BellSoft releases Alpaquita Containers for Spring Boot Alpaquita containers are based on Alpaquita Linux and Liberica JDK Lite integration and enhance your app with better speed, higher security, and improved overall performance. We designed Alpaquita Linux to run Java in containers. Alpaquita Linux is a fast and secure Linux distribution for Java. It allows creation of a docker base image size of 3.28 MB with an optimized musl libc library, memory management, and revamped Mallocs. Alpaquita containers with a glibc library are available too. Liberica JDK Standard is a default Java runtime in Spring Boot. To make it even better for cloud environments, the BellSoft team produced a new version of the runtime adapted to the cloud — Liberica JDK Lite. Liberica JDK Lite is used in Alpaquita containers to make them smaller and faster. Why do you need an Alpaquita container? An Alpaquita container immediately strengthens your Spring Boot application in the following ways: Memory: save up to 30% of RAM instantly; Cost: reduce your cloud spending; Security: covered with 24/7/365 support and LTS releases. Various comparison tests demonstrate the gap in size and RAM consumption of the OpenJDK Docker approach to containerization vs. Alpaquita Linux. Try the Alpaquita container in your app and tell us about the new level of optimization you achieve. The step-by-step guide to measuring the impact of the Alpaquita container on your Spring Boot application is available here. About BellSoft BellSoft delivers the most complete Java experience. We are the only OpenJDK vendor that supports current LTS Java versions, legacy JDK 6 & 7 and Liberica NIK, the GraalVM-based native-image compiler. Our Java platform, Liberica JDK, provides a unified Java experience across the organization, delivering a more secure, reliable, and cost-effective approach to application development on any platform and in any environment. BellSoft is one of the leading contributors to the OpenJDK, and Liberica JDK is the runtime of choice for VMware, Spring Framework, JetBrains, and millions of users around the world. BellSoft serves millions of use cases and global brands across every industry, helping companies build for the future every day. For more information, visit www.bell-sw.com. - [Repetitive mistakes in Java code](https://bell-sw.com/blog/repetitive-mistakes-in-java-code/): It’s no secret that programmers make mistakes in the code: after all, to err is human. But errors cost time and money. If an application works incorrectly due to bugs, it takes time to reproduce the problem, nail down the mistake, fix it, and redeploy the application. The amount of support requests increases, and customer satisfaction goes down. The developers feel annoyed and depressed when they see that the program doesn’t work as expected. So avoiding mistakes or discovering them as early as possible is natural. Common caveats There are different kinds of mistakes. Some are complex and unique and happen once in a lifetime. On the other hand, many mistakes are repetitive, and developers stumble on them again and again. Here’s a simple example. Imagine that you have a method that accepts a numeric parameter, and you want to perform a range check before further processing. For instance, your method accepts a percentage of progress and updates a progress bar in the user interface. It may look like this: void updateProgress(double percent) { if (percent < 0.0 && percent > 100.0) { throw new IllegalArgumentException("Invalid value: " + percent); } // ... actual UI update logic goes here ... } Do you see the problem? Here, && operator was used instead of ||. As a result, the range check doesn’t do anything because no number can be less than zero and greater than 100 simultaneously. When I show this sample to developers, some say: “Oh, come on! No professional developer would make such a stupid mistake!” Yet, I’ve seen dozens of times when && was mistakenly used instead of ||, or vice versa. These errors were found in production codebases maintained by professional developers. Spotting such a mistake in a short code snippet is not difficult, but when you write or review hundreds of lines of code, it’s easy to use the wrong operator without even noticing. Ways to avoid repetitive mistakes What can developers do to avoid such mistakes? Well, this particular problem is caught by IntelliJ IDEA’s built-in static analyzer. If you use it to write the code, you should not set up anything; it will work out of the box. Don’t ignore the highlighting, and pay attention to what IDE says to you: Highlighting the problematic code in IntelliJ IDEA However, static analysis is limited. It knows a little bit about numbers, but it knows less about strings. For example, consider you are writing another method that needs to process lines, skipping comment lines, which start either with ‘#’ or with ‘//.’ You may write something similar to: void processLine(String line) { if (line.startsWith("#") && line.startsWith("//")) { // comment line: skip it return; } // ... process non-comment line ... } The same mistake is made here: && is used instead of ||, so no lines will be skipped. But now, the built-in static analyzer in IntelliJ IDEA doesn’t help you: it’s not smart enough to reason that no line can start with ‘#’ and with ‘//’ at the same time. You can try out bleeding-edge technology such as the AI Assistant, which uses a large language model like ChatGPT under the hood. Currently, it’s provided as an IntelliJ IDEA plugin. After installing it, you may invoke “AI Actions → Find potential problems” from the context actions menu when staying on the method declaration. For this particular code sample, it does its job pretty well: IntelliJ IDEA AI Assistant plugin at work It found the problem, explained it, and suggested how to fix the code. Looks great! However, you cannot feed every single piece of your code you write to the AI assistant and check its output. The programming process will become unbearably slow. Also, if you ask the AI assistant to find the problems in the correct code, it tries to find at least something, even if it’s not a real problem. So you’ll have to read all the AI assistant’s output to understand which issues are actual and which are just nitpicking. This may take more effort than just rereading your code and trying to find the problems yourself. The best solution is to be on full alert In my opinion, the best solution is to be prepared. As a programmer, you should know in advance the possible caveats. For instance, whenever you use && or ||, you should automatically consider whether the operator is correct. You can check a couple of input values in your head and see whether the result of the condition is appropriate. Or you can write a couple of unit tests to ensure the condition works as expected. There are not always enough resources to cover every bit of your code with a unit test, so it’s good to know where errors are more likely to hide and which code constructs are more critical to test. A while ago, I started to record and categorize repetitive Java mistakes and ways to avoid them. Eventually, this turned into a book, which is almost finished now. The book is called “100 Java Mistakes and How to Avoid Them.” The part of it is already available on the Manning publisher website. Although the book targets middle-level Java developers, it could interest juniors and advanced students. On the other hand, more experienced programmers may also learn something new from it. For BellSoft blog readers, a permanent discount code is available: bellsoft35. Use it to get the book with a 35 % discount unless there’s an ongoing sale, which provides an even better deal. - [BellSoft sponsors SpringOne conference at VMware Explore 2023](https://bell-sw.com/blog/bellsoft-sponsors-springone-conference-at-vmware-explore-2023/): BellSoft announced a collaboration with VMware in 2020, a start of a longer journey to ease and improve the OpenJDK development practice, adapting toward the community’s needs. BellSoft fortifies its OpenJDK product with qualified technical support to ensure the security of your application. The initial engagement secured VMware Tanzu users with our OpenJDK distribution, Liberica JDK, and brought full JDK support for VMware Tanzu. The collaboration was further complemented with Liberica Native Kit (Liberica NIK), which enriched the experience of all Spring Native users and stimulated the development of additional GC implementation for the Native Image technology, ParallelGC. VMware and BellSoft’s cooperation contributed to today’s popularity of Spring & VMware Tanzu solutions among Java developers. “BellSoft is proud to be part of major improvements in OpenJDK. We strive to deliver the complete Java journey for VMware Tanzu and Spring developers via our products and support. This year, we are attending the VMware Explore conference in Las Vegas as sponsors to exchange the latest opinions and insights on the industry’s latest demands. We will also present the products and services we deliver to all those looking for an optimized Java experience in the cloud-native world.”- Alex Belokrylov, BellSoft’s CEO. Today, BellSoft remains one of the largest contributors to OpenJDK and is entirely focused on OpenJDK products, delivering extended LTS support and an exclusive offer, Liberica JDK 6 & 7, intended for enterprises still running legacy Java runtime. BellSoft delivers a progressive OpenJDK runtime perfect for the cloud environment. - [Invalid CEN header fixed in the latest JDK 11 and 17 updates](https://bell-sw.com/blog/invalid-cen-header-fixed-in-the-latest-jdk-11-and-17-updates/): JDK-8313765: Invalid CEN header (invalid zip64 extra data field size) fixes a major regression in JDK versions 17.0.8 and 11.0.20. The patch is already available, the summary of the issue is provided below. Description The issue emerged in relation to the patch JDK-8302483: Improved ZIP64 Extra Field Validation, which provides additional validation of ZIP64 extra fields when opening a ZIP file. As a result, a ZipException: Invalid CEN header (invalid zip64 extra data field size) is thrown when opening APK, ZIP, or JAR files with several third-party tools in the following situations: Apache Commons-compress and some Ant releases create CEN Zip64 extra headers with a size of 0 when Zip64 mode is required; The BMD tool (the maven-bundle-plugin before 5.1.5) added problematic data to the extra field. Other third-party tools may add padding bytes to the extra field. Possible workaround Additional validation can be turned off completely by setting the jdk.util.zip.disableZip64ExtraFieldValidation property. The most optimal solution though is to keep the security feature turned on and install the update. Download Liberica JDK builds with the fix now We recommend updating your runtime to avoid functional regression caused by the bug described above. Click on the button below to head to the Liberica JDK Download Center and download fresh builds of Liberica JDK 11 and 17 with the patch Download Liberica JDK - [CI/CD tools for Java developers](https://bell-sw.com/blog/ci-cd-tools-for-java-developers/): CI/CD is the combination of practices for continuous integration / continuous delivery (sometimes also continuous deployment) aimed at implementing the release early, release often strategy without causing the integration hell. Incorporation of CI/CD helps to accelerate time-to-market and facilitate the life of developers, but before everything runs smoothly, we need to Change business processes; Cultivate the specific CI/CD mindset in the team; Utilize tools and solutions for process automation. This article covers the last-named topic. We will look into the essential CI/CD technologies and solutions for Java development that can be integrated seamlessly with CI/CD tools and processes. Table of Contents Maven, Gradle, Bazel Jenkins TeamCity Atlassian Bamboo and BitBucket Pipelines GitLab CI/CD GitHub Actions Azure DevOps Amazon AWS Other systems Solutions for Java development integrating into CI/CD pipeline Maven, Gradle, Bazel Build automation systems are the foundation of all the tools discussed below. Regardless of the solution you use, be it Jenkins or GitHub Actions, some other technology must run the compiler and tests. Moreover, a sound build system can substitute some of these external solutions. For instance, Apache Maven is marketed as more than a simple builder: “Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project’s build, reporting, and documentation from a central piece of information.” Maven manages projects through a build cycle, which includes several stages corresponding to the goals of CI/CD processes: validate, compile, test, package, verify, install, and deploy. For each of them, you can use special Maven plugins (hundreds of them are on the Internet) that enable the developers to perform various tasks, from verifying the code in a repository to managing application servers. Writing your plugin, calling the code from other programming languages, or building systems is also possible. Although Maven (or any other build system) can implement the CI/CD approach for small projects, larger teams may require greater flexibility and scalability. For instance, Administrators may find a centralized web interface more convenient; Developers should have different rights within the project; Standard build methods should be accumulated in long pipelines; And so on. This is where other solutions created over almost three decades of Java existence come into play. Jenkins Jenkins is the oldest and most popular server for automating Java processes. Released in 2017 under the name Hudson, it was renamed in 2011 due to a conflict with Oracle. Hudson was transferred to Eclipse Foundation, but the project was closed in 2017. Jenkins is a classical Java application with a web interface. Its WAR file executed with Winstone (a Jetty 10.0.x servlet container wrapper) can be used on any operating system. This WAR file can theoretically be run with other servlet containers, but such configurations are largely untested. It can also be run in the cloud with Kubernetes if necessary. Jenkins implements the infrastructure as code paradigm, or to be more precise, delivery pipelines as code. You manage the project using Jenkins Pipeline, which can be created with Blue Ocean UI, a classic UI. Writing a Jenkinsfile and committing it to source control is also possible. Such pipelines can be integrated with various tools, extended with shared libraries, or used with standard technologies like Docker. Jenkins has an impressively extensive ecosystem: there are thousands of plugins for any taste and task. Writing a custom plugin is also possible. Despite considerable age, Jenkins continues to evolve and soak up all the new CI/CD trends, thus staying one of the world’s most reliable CI/CD implementations. TeamCity TeamCity is a commercial server for CI/CD automation developed by JetBrains. TeamCity competes with Jenkins, and although its source code is unavailable on GitHub, administrators may find it more convenient and performant. In the CI/CD world, TeamCity is similar to Apple. The proprietary nature of this product enables its developers to take shortcuts and solve issues innovatively. For instance, Jenkins is driven by thousands of open-source plugins, which is good in theory, but in practice, leads to incompatibility problems, poor performance, and a lack of support for many of them. TeamCity has significantly fewer plugins, but they are all polished, tested, and supported around the clock by the best JetBrains engineers. In addition, TeamCity offers a great modern web interface, built-in support for Jira, cloud integration (VMware, Amazon, Microsoft, Google), and many other features making it an optimal choice for developers. Atlassian Bamboo and BitBucket Pipelines Atlassian Bamboo is the third most commonly used automation server. For many years, it was also one of the most expensive ones. In February 2021, Bamboo Server was deprecated, leaving only Bamboo Data Server with a starting price of USD 1,200 for one remote agent (at the moment of writing this article). The most advantageous asset of Bamboo is integration with other Atlassian services, including Confluence and Jira, that became corporate standards in modern software development. Although Bamboo has fewer features than other solutions, smooth integration with other ecosystem products makes it no less helpful than Jenkins and TeamCity. However, Bamboo has lost its position with the appearance of Bitbucket Pipelines, also belonging to Atlassian and integrated into its ecosystem. The solution is similar to GitHub Actions and GitLab CI/CD, with a few nuances. For example, the build process is supported only on Linux, and the free tier includes fewer minutes. To sum up, Atlassian’s CI/CD solution is useful for companies already using other ecosystem products. GitLab CI/CD GitLab is a version control system with a web interface, whereas GitLab CI/CD is a toolset for organizing CI/CD within this system. Unlike Jenkins, where developers operate with external repo URLs, the CI/CD customization with GitLab begins from the project files in the interface, which is very convenient. The fact that GitLab is an integrated environment with repository management gives it a competitive edge over similar CI/CD systems without version control functionality. If we compare GitLab to GitHub, we’ll notice that they approach the matter differently. GitHub uses GitHub Actions and external CI/CD platforms (Jenkins, CircleCI, TravisCI, etc.). With GitLab, however, these features are integrated and can be run locally. At the same time, GitLab doesn’t have equal GitHub “actions” but substitutes this functionality with Auto DevOps, a set of pre-configured features and integrations that enable the developers to build, test, deploy, and monitor projects. Regarding costs, GitLab implements the open-core model: there’s a free open-source GitLab Community Edition and a more advanced GitLab Enterprise. Developers can install GitLab on-premise for free. On the contrary, GitHub may have free private repositories, but they are stored in the cloud, and on-premise GitHub Enterprise Server comes at a high price. Speaking of the cloud, GitLab can be used for cloud deployments at standard market prices. GitHub Actions GitHub Actions is one of the most popular cloud CI/CD services at the moment of writing this article. GitHub is the heart of the modern Open Source community, whose servers host significant volumes of open-source code. So whichever CI/CD solution the platform offers, it is worth paying attention to. Like in the case of GitLab, GitHub Actions is aimed at building, testing, and deploying projects directly from the repository interface. In the same way, developers describe steps in separate YAML files and assign parameters to a set of predetermined actions. One of the exciting features is hosted runners, which enables developers to run their code on virtual machines with the possibility to choose an operating system, install the necessary software, etc. Small open-source projects with private repositories can use these virtual machines for free if they don’t exceed the limit of 2,000 minutes and 500 Mb. Any usage beyond these limits will be billed. Another remarkable feature is the possibility of loading new actions from the GitHub Marketplace. GitHub Actions has a large community that created thousands of configurations available on the marketplace. For instance, if you want to install Liberica JDK, there’s a free setup-java action for this purpose: you only have to set distribution: ‘liberica’ in its configuration. The beauty of this approach is that the source code of configurations is hosted on GitHub, so anyone can participate in their improvement by creating pull requests. Azure DevOps Azure DevOps came out of Visual Studio Team Services (VSTS). Even though both GitHub and Azure DevOps are Microsoft products, they have several notable differences described in the official documentation. Some of these features are absent in GitHub Actions or implemented differently regarding user experience. For example, GitHub Actions doesn’t allow creating Service connections — abstract credentials for external systems. On the other hand, it is impossible to connect to external cloud providers (not only Azure) without a password in Azure DevOps. The complete list of Azure DevOps features can be found here. In general, GitHub Actions and Azure DevOps have different atmospheres around them: GitHub Actions is more about freedom, open source, and open web interfaces and solutions, whereas Azure DevOps preserves the spirit of a seasoned enterprise solution for the most demanding corporate workloads. Amazon AWS Amazon offers multiple products and services for setting up the CI/CD pipeline. Ultimately, any platform used for production deployment becomes part of the continuous deployment process. Take AWS CodePipeline, for instance, which is a fully managed continuous delivery service integrated into the Amazon AWS ecosystem. It enables developers to pull the code from CodeCommit/ECR/S3, build and test it with CodeBuild, and deploy it with CodeDeploy/Beanstalk/ECS/Fargate. You can also use CloudFormation actions or deploy serverless applications with AWS Lambda. Although you can push the code to CodePipeline directly from GitHub, the strength of Amazon lies in its internal ecosystem. In AWS, all products are interconnected, which saves users a lot of time and money otherwise wasted on building a complex system of services with good SLAs from scratch. Interestingly, the system benefits can turn into disadvantages: the company that migrated its workloads to AWS may find it extremely difficult to switch providers due to the lack of direct equivalents and drop-in replacements on the market. Other systems We described some commonly used and tried-and-true solutions, but numerous platforms for automating CI/CD exist. There’s a high chance you use solutions from the list below: Travis CI CircleCI CodeShip GoCD Drone CI Concourse CI Buildkite Buddy Semaphore CI Harness Codefresh JFrog Pipelines Some offer valuable integrations (similar to Liberica JDK and GitHub Actions). Still, they all support Java so that you can build and test any application, in our case, developed with Maven or Gradle. The list can go on. Every year, new solutions come to life. Despite only sometimes making it to TOP-10, they often offer unique features or vision (for instance, Concourse views the build process as a distributed Makefile). So even if you build Java applications with good old Jenkins or TeamCity, it is worth glancing at new candidates occasionally. This way, you may find a better-suited system for your business tasks. Solutions for Java development integrating into CI/CD pipeline We looked into platforms that help IT teams build a CI/CD pipeline. But the solutions you use for developing and deploying your corporate Java application — the runtime, containers, additional utilities, — can either enhance or offset the benefits of automation. BellSoft offers a variety of solutions that can be seamlessly integrated into your CI/CD processes and accelerate them even more: Liberica JDK, a Java runtime with the widest range of supported system configurations and multiple installation paths: through container registries (images for all Linux OSs are available), Linux repositories, package managers, or REST Discovery API. You can also get ready instances in several most popular cloud environments. Ready container images with Alpaquita Linux (100% Alpine-compatible, musl- and glibc-based) and any Liberica JDK LTS version. These images enable you to build microcontainers to optimize cloud resource consumption, and you can also install additional required packages from the repository such as maven or gradle to fit your workflow. Base Alpaquita images and images for Python and C/C++ development are also available. Liberica Native Image Kit, a GraalVM-based native image compiler, which is also available as a container image for musl or glibc. Buildpacks to automate the containerization process. Software Bill of Materials (SBOM) for BelSoft’s products. An SBOM is an indispensable component of software supply chain security, and it also integrates into vulnerability management practices, enabling the DevSecOps experts to monitor infrastructure security and promptly react to increased risks without disrupting the workflow. The technologies your team uses for development and deployment are not less important for building a smooth CI/CD pipeline. Select a perfect combination for your corporate IT infrastructure, and you will be able not only to accelerate and automate the work processes, but also optimize resource consumption and increase performance of your project. - [An overview of SLSA for software supply chain security](https://bell-sw.com/blog/an-overview-of-slsa-for-software-supply-chain-security/): The surge of hacker attacks on software supply chains gave impetus to increasing the transparency of the IT infrastructure and accountability of software vendors. One instrument for this purpose is a software bill of materials (SBOM), which provides essential data on all libraries used to develop a software product. But an SBOM is a collection of data, which should ideally be integrated into higher-level security processes represented by the Supply-chain Levels for Software Artifacts (SLSA) framework. The article overviews this novel cybersecurity technique and its advantages for organizations looking for industry-standard ways to identify and mitigate security risks. Table of Contents What is Supply-chain Levels for Software Artifacts (SLSA)? How SLSA complements an SBOM SLSA requirements SLSA Levels Getting started with SLSA Conclusion What is Supply-chain Levels for Software Artifacts (SLSA)? SLSA is a framework providing specifications and common language that enable software users to ensure the code is trustworthy and wasn’t tampered with. Google proposed it in cooperation with OpenSSF in 2021 in answer to the concerns about the integrity of software supply chains, which are related to the ever-increasing number of supply chain attacks. SLSA is beneficial for everyone involved in producing or utilizing software: Software developers and vendors writing open-source or proprietary software can ensure that the product they produce reaches their users untouched and as intended; Infrastructure providers can provide a reliable SLSA-compliant build platform that connects software vendors to consumers; Consumers (both developers using software libraries and tools for their applications and organizations integrating a software product) can make an informed decision about the integrity and trustworthiness of the software package. How SLSA complements an SBOM A software bill of materials and SLSA are two complementary tools that help to increase the transparency of a supply chain and guarantee the integrity of software packages. An SBOM is often compared to a list of ingredients because it provides a list of dependencies used for software development. In this regard, SLSA is a set of guidelines that help software manufacturers ensure and prove that the contents of a package correspond to the ingredient list. By following and attesting to the SLSA requirements, developers confirm that No malicious code was introduced into their product at any development stage; The packages are built with the expected build platform; The list of dependencies wasn’t tampered with. As a result, end-users receive the list of dependencies with the data on their provenance and existing vulnerabilities. They can decide whether they can trust the supplier by judging their SLSA compliance. SLSA requirements SLSA consists of requirements for producing artifacts (immutable data blobs, such as container images, files, git commits, etc.) at various levels, discussed in more detail in the section below. The requirements are split between Producers — organizations, teams, or individual developers who own and release software, who must Choose a build platform satisfying the requirement for a necessary SLSA level; Consistently build artifacts; Distribute provenance to consumers in the form of attestations. Build platforms — infrastructures transforming code into packages, which must Generate provenance describing how the package was produced; Isolate between builds to guarantee that the build was executed without external influence. SLSA Levels The SLSA framework is divided into tracks and levels. Current version 1.0 defines only the Build track (describing the artifact’s provenance), with more tracks covering the whole supply chain planned for future releases. Each level represents a higher degree of software security, with Level 0 assigned to a system without any implemented security measures. For instance, security levels for the Build track are described as follows: On Level 1, the provenance exists. The build system can automatically generate provenance; developers follow a consistent build procedure and provide customers with provenance data. On Level 2, the builds run on a hosted platform that generates and signs the provenance, and the authenticity of provenance is validated by downstream verification. On Level 3, builds run on a hardened build platform with extra protection against tampering (preventing runs from influencing one another and making secret material used for provenance signing unavailable to user-defined build steps.) Each level builds upon the previous one, gradually enabling the developers to fortify their systems based on specific requirements. Getting started with SLSA Let’s look at the process of integrating SLSA into your workflow: The first step is to select and implement a SLSA-compliant build platform if you aren’t using one already. Different platforms correspond to different SLSA Levels (for instance, GitHub Actions is suitable for Level 3.) Secondly, generate provenance data. A simple build configuration file is enough for Level 1. Some platforms, such as FRSCA or GitHub Actions, provide tools for generating signed provenance and can be applied at higher SLSA levels. The next step is to make the provenance metadata available to users in the form of SLSA attestations. There are several ways to distribute the attestations. For instance, some repositories support publishing provenance files together with the artifact. Another option is to include provenance in source repository releases (for example, GitHub or GitLab.) Conclusion As you can see, implementing the basic SLSA Level is not complicated, but you get the benefits of a more transparent and reliable software production cycle. So, the first step towards greater transparency and credibility of a software supply chain is to generate an SBOM for your software supply chain and then gradually implement measures to reach SLSA compliance. To aid Java developers in this task, BellSoft provides Alpaquita Cloud Native Platform, a comprehensive solution for developing and deploying cloud-native Java applications with Liberica JDK, an OpenJDK distribution with a wide range of supported platforms and versions, Alpaquita Linux, a lightweight Linux distribution with enhanced flexibility and security, Liberica Native Image Kit for converting Java applications into native images and Enterprise support from one vendor. All products are developed per the Secure Development Lifecycle (SDL) practices and receive regular security patches. We will also provide a software bill of materials, helping you to keep your infrastructure transparent and integrate modern cybersecurity solutions. Contact us, and we will be happy to answer any questions. Contact us - [Top-9 cybersecurity threats in 2023](https://bell-sw.com/blog/top-9-cybersecurity-threats-in-2023/): Global digitalization increases the surface for cyberattacks that become more relentless, sophisticated, and capable of bringing down hundreds of enterprises and harming millions of people at once. Forewarned, forearmed: We gathered the most prevalent cybersecurity threats in 2023 so that you can verify whether your current security policies are capable of warding them off effectively. Table of Contents A quick note on terminology Attack vectors Social engineering Malware Drive by download Spam Insider job Exploitable vulnerabilities Zero-day exploits Known vulnerabilities Credentials breach Misconfiguration Tune your Java apps for enhanced security! A quick note on terminology First thing first, let’s dig deeper into the key concepts: A threat is a possible disruptive action that an attacker can carry out; An attack is an attempt to gain unauthorized access to a system with malicious intent (data compromise or destruction, system disruption, etc.); A vulnerability is a flaw in an IT infrastructure that makes it open to harmful impact; An attack vector is a specific method, scenario, or path, associated with vulnerable components of the target system and used for gaining unauthorized access to the system. For convenience, we will group the threats into attack vectors and vulnerabilities. Note that threats often go hand-in-hand (e.g., spam can be used as a social engineering technique to spread malware), or they may be embedded into each other (e.g., malware may exploit zero-day vulnerabilities). Attack vectors 1. Social engineering Social engineering covers all techniques aimed at manipulating human psychology to gain sensitive information, access restricted information, or coerce victims into performing desired actions (download a malicious file, authorize a transaction, etc.) Social engineering relies heavily on such human emotions as curiosity, greed, fear, and compassion, with criminals following a typical pattern of Gathering information about the victim; Engaging in communication; Exploiting the victim’s weak spots to perform the attack; Disappearing and covering the tracks. Not all social engineering schemes require such thorough preparation. Sometimes, one excellently crafted email mimicking a legitimate message from the bank is enough to lure a person to a malicious site. 2. Malware Malware is a program containing malicious software designed to steal data or destroy/disrupt computer systems. Hackers often use vulnerabilities in operating systems, applications, websites, or networks to plant malicious code. Malware attacks are becoming increasingly devastating: a single malware program can damage hundreds of organizations and cause millions of dollars in losses, like the infamous WannaCry or NotPetya did. Types of malware include Ransomware — blocks access to data or the whole system until a victim pays a ransom for a decryption key; Spyware — secretly gets installed on user’s device to steal sensitive information; Computer viruses — attach to other programs, self-replicate, and spread to other devices to steal data, damage systems, or install additional malware; Cryptojacking — steals computer resources to mine cryptocurrency; Backdoors — get around authentication procedures to give attackers remote access to a system. The first line of defense against malware is regular software updates to eliminate vulnerabilities that cybercriminals can use. Furthermore, it is recommended to use modern security solutions such as NGAV or NIPS discussed below to detect and block any suspicious activity in the network promptly. 3. Drive by download Drive by download refers to the unintentional downloading of malicious software, files, or code on a device without the user’s awareness. Drive-by-download attacks can happen in two ways: Users authorize the download by Clicking on a link masked as a notification from a source they trust: an email from the bank, a security alert, etc.; Downloading free software bundled with malicious files; Users are unaware of the download. Hackers exploit a vulnerability in a website and plant a malicious component. Users visit a seemingly legitimate but already compromised web page and trigger the download without prompts. As drive-by download attacks exploit existing vulnerabilities, developers can protect their sites by updating the software regularly and removing obsolete components that may be filled with security flaws. 4. Spam Spam is an unsolicited message sent over the internet, usually to many users. Although, in many cases, spam is sent by businesses for advertising, it can be used as a social engineering technique to spread malware or get a grip on personal data. Malicious actors often use the following types of messages to try and lure the victims into giving up confidential information: Spoofing is when the message is masked as coming from a reliable source: your bank, employer, colleague, a known company, etc. The scammers imitate the logos, brand identity, or tone of voice for higher credibility to make a user click on the malicious link or take any other desired action (e.g., sending money or sensitive data); Antivirus messages are disguised as warnings from the antivirus system urging users to take action against an allegedly discovered virus; Emails asking for money to help somebody in need; Prize scams are messages announcing that a user won a prize, lottery, etc., and urging users to reveal their personal information or wire the money to get the award. The best way to minimize the risk of a successful spam attack is to never click on any links or attached files from dubious sources. If the email seems to be coming from a legitimate source, verify it by checking the email header (containing metadata on the letter’s origin) or contacting the company/person for confirmation. 5. Insider job Insider threat originates from within an organization and is caused by unintentional (negligent) or intentional misuse of system access by regular employees, privileged users, or third parties such as subcontractors, business partners, etc. Insider threats can cause more significant financial damage than a data breach caused by an external actor. The risk of a data breach caused by negligence can be mitigated by raising awareness about proper security measures among employees, but what about malicious actors stealing corporate data out of spite or for financial gains? While it is generally hard to predict which employee is capable of committing the crime, the general protection measures include Monitoring user activity and behavior. One way to do that is to utilize user and entity behavior analytics (UEBA) technology, which analyzes various data to form a model of normal user behavior and detect any anomalous activity. Implementing Privileged access management (PAM) to control permissions, roles, and user access to sensitive data. Exploitable vulnerabilities 6. Zero-day exploits Software vulnerabilities are weaknesses or flaws in the code that an attacker can use as an entry point to the system. A zero-day vulnerability is a weakness discovered and exploited by hackers before software developers became aware of it, so there’s no patch available. Zero-day exploits often serve as the basis for malware attacks, as in the recent case of two Apple zero-day vulnerabilities used for planting spyware on iOS devices. Zero-day exploits are extremely harmful because they can go unnoticed by users or security experts for a long time, and the system remains vulnerable until the patch is issued. Given their nature, zero-day exploits cannot be completely prevented, but there are ways of minimizing the risks: Implement a good firewall to detect and block suspicious traffic promptly; Follow the principle of least privilege by restricting user access to files and resources and permissions strictly necessary to do their jobs. This way, even if one part of a system is compromised, the impact will be limited; Use cutting-edge security tools like a next generation antivirus (NGAV) and a network intrusion protection system (NIPS). In contrast to traditional antiviruses that can act upon a vulnerability only when it is added to their database, NGAV takes advantage of predictive analytics and threat intelligence to identify suspicious behavior and previously unknown malware. NIPS ongoingly monitors the system for suspicious activity and takes action against it by blocking it, alerting the administrators, resetting the connection, etc. Put a patch management system into practice. It will not protect against zero-day exploits per se but will allow you to implement the patches as soon as they come out, thus limiting the exposure window. 7. Known vulnerabilities The only difference between zero-day and known vulnerabilities is that the latter have already been identified and patched. But why do they still present a threat? Although software developers constantly monitor their products and release emergency or regular patches, users often neglect to update the software, thus leaving the backdoor wide open. The situation is even worse with large projects using hundreds of dependencies. Regular software updates are the basics of cybersecurity that should come before any top-notch security tools. But what if a project uses open-source libraries that haven’t received any updates for months? An excellent solution helping to increase the transparency of IT infrastructure is a software bill of materials (SBOM). An SBOM is a collection of data on all components used to build a software product, including the supplier, version, vulnerabilities, and licensing. An SBOM helps to promptly react to new or existing vulnerabilities and take corrective measures. BellSoft provides Liberica JDK updates for all LTS versions (8, 11, 17), a current version, and legacy OpenJDK 6 & 7. If you deploy containerized application and would like to secure both Linux and Java, consider Alpaquita Cloud Native Platform, an enterprise-grade solution that includes Liberica JDK Lite optimized for cloud deployments; Alpaquita LTS, a lightweight Linux with two libc implementations (musl and glibc) and additional security features; Liberica Native Image Kit for native image generation; 24/7 service from a leading OpenJDK contributor with emergency and off-cycle patches both for Linux and JDK. 8. Credentials breach Stealing credentials is the easiest way to access a system, so user data remains a tempting target for hackers. According to 2023 Verizon Data Breach Investigations Report, 81% of hacking-related data breaches leveraged stolen or weak passwords. As far as weak passwords are concerned, the Cybernews Investigation team analyzed 15+ billion passwords in public data breaches and found that the most popular one was “123456,” followed by “123456789” and “qwerty.” Credentials can be stolen by breaching a database, but sometimes, it is a matter of human error. For instance, employees can send sensitive information via email, leave their computers unattended, or write the login credentials on paper. Large collections of stolen credentials sold on the black market can be further used for credential stuffing — a kind of a brute-force attack when stolen login credentials are used to access the user accounts. To minimize the risk of credentials breach, it is recommended to Use multi-factor authentication; Enforce strong password policy in a company and educate employees on basic security practices; Implement the least privilege principle described above to limit the harmful impacts. 9. Misconfiguration Misconfiguration refers to mistakenly configured or missing / default settings, which create an entry point for unauthorized access. Configuration errors can be encountered in an application, network, or cloud environment, with examples including: Weak or missing encryption; Default username, password, and other setting; Improper error handling and logging; Coding mistakes, such as wrong XML parser configuration in Java that may lead to XSS attacks; Improper API configuration. The risk of these errors can be mitigated by implementing strict configuration rules. Another good practice is to adopt a zero-trust architecture based on the principle “never trust, always verify.” It means that the trust within the corporate network is never granted implicitly. The zero trust approach implies using strong authentication, minimum privileges, granular access, and other techniques described in detail in the NIST Special Publication 800-207. Tune your Java apps for enhanced security! Although no solution guarantees 100 % protection from all cyber threats, the best risk mitigation techniques include: Regular software updates; Granular access to system components; Implementation of bleeding-edge security solutions; Awareness about the emerging dangers. Find even more case studies, surefire recommendations, and valuable techniques for protecting your applications in our Java application security guide. - [Liberica JDK 21 LTS release: a lasting foundation for your Java application](https://bell-sw.com/blog/liberica-jdk-21-lts-release-a-lasting-foundation-for-your-java-application/): We are happy to announce the general availability of Liberica JDK 21, the new LTS release that will enjoy BellSoft’s extended support until March 2032! Download the builds now or read on to discover why migration to Liberica JDK could be the turning point for your enterprise Java development. Download Liberica JDK Liberica JDK 21 supports the Coordinated Restore at Checkpoint API. The feature help to minimize Java application startup to mere milliseconds. Download Java with CRaC and experiment with the functionality! Table of Contents JDK 21 — more performant, secure, and user-friendly What better occasion for getting off Oracle Java license? Life after Oracle Java — get the most complete Java experience with Liberica JDK Download Liberica JDK 21 now! JDK 21 — more performant, secure, and user-friendly The new LTS release includes 15 new and improved features to enhance performance and accelerate development by enabling programmers to write more concise and manageable code. We described every JEP in detail in our previous article, but let’s do some cherry-picking! Boosted performance: Virtual threads (finalized feature) enable the developers to create thousands of lightweight threads, which can be monitored and controlled like the standard application threads, bringing the performance and manageability of high-throughput applications to a new level. Generational ZGC stores young and old objects separately and collects young ones more often, thus reducing the GC and heap memory overhead. Enhanced security: The new Key Encapsulation Mechanism API allows Java developers to use a modern cryptographic technique of securing symmetric keys with asymmetric or public key cryptography. KEM is a promising method for protection against quantum attacks, which may present a significant security threat in the near future. Increased convenience: Unnamed patterns and variables enable the developers to substitute unnecessary type and name of record components and unused variables with the underscore character _, which increases code readability and conciseness. Record patterns are used to destructure instances of record classes and create nested patterns, enabling more declarative data processing. What better occasion for getting off Oracle Java license? A new version plus another vendor equals new life for your Java apps. But if you have used Oracle Java for years, should you bother with migration to OpenJDK? The answer is “yes, you should,” and here are the TOP 3 reasons why: The new Java SE Universal Subscription introduced by Oracle in January 2023 makes companies acquire Java licenses based on the number of full-time, part-time, and temporary employees (including those of their agents and contractors). As a result, a medium-sized company may experience up to a 1,400 % increase in Java costs! Oracle made a habit of changing Java licensing conditions every two years, keeping their customers on tenterhooks. You have to acquire a license to receive quarterly security updates for your runtime, which are free of charge with OpenJDK vendors. And the list goes on! If you are still in doubt, download the summary of 10 indisputable reasons to say goodbye to Oracle Java. Life after Oracle Java — get the most complete Java experience with Liberica JDK You can leverage your Java expenses and migrate to OpenJDK — the codebase is the same as Oracle Java’s, so the key differences are in licensing (OpenJDK distributions are 100 % open source and freely available) and additional offerings, so the choice should be based on your business needs. Liberica JDK is the only Java runtime that offers the most complete Java experience with: The broadest range of supported platforms and versions. BellSoft supports all LTS versions, the current version, legacy Java 6 & 7, and GraalVM Native Image — even if your services run on different Java versions, we’ve got you covered. Affordable prices. You can use Liberica JDK for free or enjoy our high-powered support with flexible plans customized for your requirements and 24/7 service directly from Java engineers. Three flavors: Liberica JDK Standard, Liberica JDK Full with JavaFX, and Liberica JDK Lite optimized for cloud deployments, each tailored to specific purposes. Additional instruments and technologies, such as Liberica Native Image Kit for creating native images, Liberica Administration Center for monitoring and updating a corporate Java fleet, Liberica JDK Performance Edition for those who want to enjoy the performance perks of fresh Java versions and upgrade JDK at their own pace. Alpaquita Containers are lightweight containers for Spring Boot applications that can help you save up to 30 % of RAM instantly. Find out even more Liberica JDK features in our overview of Oracle Java alternatives or get a comparative table of commercial offerings of OpenJDK vendors vs Oracle and discover the value you get for your money. Download Liberica JDK 21 now! Ready to turn the page on endless helter-skelter with Oracle Java licensing? Download Liberica JDK 21 and enjoy all the benefits of a progressive Java runtime from a leading OpenJDK contributor! Download Liberica JDK - [Devoxx Belgium 2023: Your Java-focused guide from BellSoft](https://bell-sw.com/blog/devoxx-belgium-2023-your-java-focused-guide-from-bellsoft/): Devoxx Belgium, organized by the Belgian Java User Group (BeJUG), is considered the largest vendor-independent Java conference in the world. Started in 2001 as a Java developer conference series, it now encompasses many exciting topics around Java and beyond and regularly gathers over three thousand IT enthusiasts and around two hundred renowned speakers from all over the world. BellSoft is excited to be a silver sponsor of the 20th-anniversary edition of Devoxx Belgium 2023, scheduled for 2–6 October. And this is going to be an unforgettable event! Taking place in the Kinepolis, one of the largest European cinema complexes, five conference days are to be packed with 200+ talks, deep-dive sessions, and hands-on labs from 199 crème de la crème experts. Want to get it all? We understand. But as many sessions are held in parallel, and no Time-Turner invention is expected in the near future, we recommend choosing the talks you want to attend beforehand. To help you with the choice, we prepared an overview of this year’s activities and top must-visit presentations for Java devs! Table of Contents This year’s exciting agenda Top Java sessions to visit Don’t forget to chill out and take advantage of networking opportunities See you at the conference! This year’s exciting agenda Devoxx Belgium 2023 is dedicated to the latest and greatest trends in Java development and many other IT fields. As such, this year’s presentations are divided into ten tracks: Architecture Build & Deploy Data & AI Development Practices Java Mind the Geek, including the whole lot of future-is-here topics such as biological computing, cybernetics, etc. People & Culture Security Server Side Java UI & UX Top Java sessions to visit With the variety of topics to be covered this year at Devoxx, one doesn’t know where to look first. Below are several talks on the hottest trends — Java 21, Spring Boot 3, Kubernetes, GraalVM, supply chain security — Java developers shouldn’t miss out on: “Java 21” by Nikolai Parlog (Oracle), on Monday from 09:30 – 12:30; “Spring Infrastructure Deep Dive: Virtual Threads, Checkpoint Restore, Native Images” by Juergen Hoeller, Sébastien Deleuze, and Arjen Poutsma (VMware), on Monday from 13:30 – 16:30; “Dockerfiles, Buildpacks, Jib and more ... what's the best way to run your Java code in Containers?” by Matthias Haeussler (Novatec Consulting GmbH), on Monday from 18:20 – 18:50; “Everything you need to know about GraalVM Native Image” by Alina Yurenko (Oracle) and Fabio Niephaus (Oracle Labs), on Tuesday from 09:30 – 12:30; “A hitchhikers guide to observe (Java) applications in Kubernetes” by Tiffany Jernigan, (VMware) and Matthias Haeussler, on Tuesday from 13:30 – 16:30; The Spring BOF, which is a great opportunity to discuss all things Spring with the Spring engineers, on Tuesday from 19:05 – 20:05; “GraalVM Native Image: Benefits, Challenges, and the Future” by the leading GraalVM engineers from Oracle and Red Hat, on Wednesday from 15:10 – 16:00; “Bootiful Spring Boot 3” by Josh Long (VMware), on Wednesday from 17:50 – 18:40; “Project Loom: Modern Scalable Concurrency for the Java Platform” by Alan Bateman (Oracle), on Thursday from 13:50 – 14:40; “Securing the Supply Chain for Your Java Applications” by Thomas Vitale (Systematic), on Friday from 10:35 – 11:25. And, of course, don’t miss the presentations of BellSoft’s Senior Performance Architect, Dmitry Chuyko. Dmitry will Guide you through crucial container optimizations to reach essential KPIs in the cloud in the “Why you need performance tests for proper Kubernetes scaling” tools-in-action session on Monday from 17:35 – 18:05 in Room 8 and Uncover the amazing capabilities and prospects of Arm servers for cloud-native Java apps in the “Java on Arm. New horizons” conference talk on Wednesday from 16:40 – 17:30 in Room 5. Don’t forget to chill out and take advantage of networking opportunities Large conferences such as Devoxx are a great way to do some networking: Mingle with peers, Talk to the top experts in person, Establish friendly and business connections. We absolutely love this part! Come and meet us in person at booth 25. We will be happy to chat with you about all things Java — performance, security, cloud, and so much more! But remember to take a break between soaking up new knowledge and networking. On Wednesday evening, there’s an informal reception with fries and beer, and on Thursday evening, you can catch a movie in Room 8. See you at the conference! We look forward to sharing our expertise on cloud-native Java, including performant microcontainers, JVM optimizations, and native images. Visit us at booth 25 and learn how to get the most complete Java experience with our tools, solutions, and services! See you at Devoxx Belgium 2023! - [Java Community Process (JCP): Shaping the future of Java](https://bell-sw.com/blog/java-community-process-jcp-shaping-the-future-of-java/): How Java adapts to present circumstances and reacts to developer and business needs is almost uncanny and makes you perceive it as an intelligent being. Jokes aside, Java owes its rapid transformation to the immense community around it, where anyone, individual or enterprise, can pitch in to improve the platform. And this stream of contributions, big and small, is conveniently collected, sifted, and guided by the established framework — the Java Community Process (JCP). This year, the JCP turns 25: a great occasion to look behind the scenes of Java’s evolution! Table of Contents About the JCP program Key concepts Java Community Process Members So what exactly is the process? JSR process timeline Become a JCP member and take part in enhancing the Java platform! About the JCP program The Java Community Process (JCP) program was established in 1998 to provide a solid framework for developing the Java platform. And after 25 years, it still goes strong and delivers excellent results. What lies beneath its success? Community involvement — anyone can join the JCP and participate in developing or reviewing Java specifications regardless of their status and regalia, thus making the process open and inclusive. Transparency — every new feature goes through several public reviews before making it to the standards, and all development stages are well documented and accessible to anyone. Responding to business demands and far-running trends — the JCP guides Java evolution in line with the tendencies in the IT industry and the emerging needs of developers and organizations worldwide relying on the Java platform. Separating the wheat from the chaff — the JCP helps to decide what is worth working on. For instance, the proposed feature has to be helpful for many developers, and the contributed code has to be of high quality and meet the standards. Standardization — the meticulous process of JSR development, assessment, and testing provides intercompatibility of Java technologies across platforms and implementations. Key concepts The JCP program operates with specific concepts, so let’s dig into some key terms to grasp the JCP essence better. To become a JCP member with full capabilities, you must sign a one-year Java Specification Participation Agreement (JSPA) with Oracle, which gives you the right to work on JSRs. ↓ Java Specification Request (JSR) is a proposal to develop a new or update an existing Java specification. ↓ JSRs are submitted by the program members to the PMO, Program Management Office at Oracle that administers the JCP and leads the JCP Executive Committee, having a non-voting Chair there. PMO reviews the proposed JSR and then, in case there are no issues or missing data, the JSR is posed for a JSR Review. ↓ The JCP Executive Committee (JCP EC) is a group of major stakeholders and Java community members guiding the Java evolution. The JCP EC reviews and approves the submitted JSRs and monitors the process of their development and integration into the Java platform. BellSoft is proud to be a member of the JCP Executive Committee together with Eclipse Foundation, Microsoft, IBM, Oracle, SAP SE, and other industry leaders. ↓ Each JSR is developed or revised by the dedicated Expert Group under the guidance of a Specification Lead (Spec Lead). ↓ The Spec Lead is responsible for Expert Group deliverables, i.e., the development of a specification, RI, and TCK: RI (Reference Implementation) is a “proof of concept” of a specification; TCK (Technology Compatibility Kit) is a set of tests to verify that the JSR implementation complies with the Specification. For example, OpenJDK version 17 is a Reference Implementation for the Java SE 17 Platform JSR. Consequently, the TCK test suite is utilized to prove that a given RI (OpenJDK distribution) complies with the Java SE spec. TCK-verified OpenJDK distributions guarantee issue-free migration from Oracle Java, which might be a burning question for companies dissatisfied with drastic changes to Oracle Java licensing. Java Community Process Members Any individual developer or organization can contribute to enriching the Java platform to the extent they deem fit, from providing feedback on currently developed features to joining an Expert Group working on a particular Java specification. For that purpose, the JCP program offers several roles and membership opportunities: Observers are individuals who don’t have to sign the JCPA but can review and comment on JSRs in work; Associate Members are individuals who sign an Associate Membership Agreement to contribute to JSRs and vote for the JCP EC; Partner Members are non-profit organizations (e.g., JUGs) that can sign a simplified Partner Membership Agreement to vote for or serve on the JCP EC; Full Members are individuals and organizations that sign the JCPA to be able to join an Expect Group, become a Spec Lead, or serve on the JCP EC. The JCP EC comprises the JCP members, both major organizations and individual representatives of the Java community. In total, there are 16 JCP members on the EC (Oracle has a permanent seat) plus a non-voting chair represented by the PMO member. So what exactly is the process? All JSRs submitted to JCP must go through lengthy drafting, development, revision, enhancements, and testing procedures before they become full-fledged Java specifications. Since Java development is a collaborative effort, and all voices and contributions have to be accounted for, this procedure took the shape of a well-defined JSR process framework that enables transparent cooperation between the Executive Committee, Expert Group, and JSR reviewers. JSR process timeline Stage Step Timeframe Initiation JSR submission and public review 2–4 weeks The JCP EC approves or rejects the JSR 7 days Early draft The Expert Group (EG) is formed 30–90 days The EG writes an Early Draft The Early Draft is submitted for public review and reworked based on the feedback if required Public Review A JSR draft specification goes out for a public review 30–90 days The JCP EC decides whether the draft should proceed to the next step 14 days Final Release The EG proposes a final specification draft based on the feedback No limit on the timeframe The RI and TCK are completed The JCP EC reviews and approves the Proposed Final Draft, RI, and TCK 14 days The Specification is released together with RI and TCK Maintenance Requests for specification updates are tracked On ongoing basis The JCP EC approves or declines proposed changes 30 days The specification gets updated To anyone looking at the table above, it may seem that the Java standards are updated agonizingly slowly. However, these time limits are justified as the steps encompass Extensive work by the Expert Group, Substantial engagement of the community through numerous feedbacks and elaborate discussions, A thorough assessment of the results and prospects by the Executive Committee, All of which guarantee that the specification is fully ready and can be seamlessly integrated into the platform. In addition, the Committee may conclude at any evaluation stage that further JSR development is unpromising, thus sparing the Expert Group unnecessary labor. Besides, multiple JSRs are usually developed in parallel. For instance, as of September 2023, six JSRs are in active development, and 268 are in the Final Release stage! Become a JCP member and take part in enhancing the Java platform! The joint effort of individuals and organizations makes Java more potent with every release. BellSoft made contributions an essential part of its business model. We commit fixes and patches to every OpenJDK release and are members of the key communities and organizations: JCP Executive Committee OpenJDK Vulnerability Group GraalVM Advisory Board Linux Foundation Cloud Native Computing Foundation This way, we stay in touch with the community, help make Java better for everyone, and improve our products according to users’ needs. You can contribute to the platform, too! There are numerous ways of getting involved: review the OpenJDK code and commit bug fixes, participate in Java projects, provide feedback on active JSRs, propose your enhancement, and so on. You can start by registering on the JSR site, reading the mailing lists, and taking part in discussions. And remember, even the smallest contribution can make a significant change! - [Liberica Native Image Kit 23.1.0 for JDK 21 is out](https://bell-sw.com/blog/liberica-native-image-kit-23-1-0-for-jdk-21-is-out/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 23.1.0 for JDK 21. The release contains numerous fixes and enhancements. Note that Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds are made available four times a year as before. Summary of fixes and enhancements 3,631 fixes in total: In Liberica NIK: 865 fixes; In JDK: 2 607 fixes + 159 in FX. Important changes Removed GraalVM Updater, gu, from Liberica NIK Full and Core. The tool remains in Liberica NIK Standard for backward compatibility. Removed the option --language: (to enable a specific language runtime at build time) for certain languages, such as JavaScript. Introduced a new class initialization approach (enabled with --strict-image-heap): all classes are now allowed to be used and initialized at build time (see proposal #4684 for more information). Added experimental support for foreign down calls (Project Panama). Introduced an option -H:±UnlockExperimentalVMOptions to explicitly unlock access to experimental options. Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. Download the latest version of Liberica NIK now! Download Liberica NIK - [7 tips to optimize Java performance on Kubernetes](https://bell-sw.com/blog/7-tips-to-optimize-java-performance-on-kubernetes/): Java performance optimization is a formidable task, and Kubernetes, a complex tool with its intricacies and caveats, only complicates the matter. Although modern containerization technologies and Kubernetes offer optimal default settings for the effortless entry into the cloud, you can’t take advantage of the defaults for too long, or else your instances will spin completely out of control. But don’t worry, we’ve got your back! This article focuses on proven techniques bound to help you improve the key performance indicators of your Java application running on Kubernetes. Table of Contents Set the CPU and RAM limits accordingly Distinguish types of services Use Kubernetes probes effectively Upgrade the Java version Select the appropriate Garbage Collector Use a small base OS image Reduce startup and warmup time Set the CPU and RAM limits accordingly Apart from optimizing the JVM memory footprint with options such as -Xmx, -XX:MaxRAM, -XX:+UseStringDeduplication, etc. (we discussed JVM memory configuration in more detail in our previous article), you should set the requests and limits for CPU and RAM utilized by pods and containers in Kubernetes. But selecting the proper memory limits for the application is like going between Scylla and Charybdis: the FinOps demand shrinking the resource usage to reduce cloud bills, but at the same time, meeting the Service Level Agreement (SLA) made with users or clients may require more resources. Luckily, the pieces of advice below will help you navigate these waters safely: Use load testing to understand the application’s behavior under normal conditions and stress testing to measure resource consumption at peak performance. Tune the settings accordingly. Don’t set the limits too low. Even if your application consumes fewer resources under stable load, it needs more CPU for warmup and peak loads. Determine the lowest requirements to meet SLOs (Service Level Objectives the developers should reach to meet SLA). Utilize tools to study the relevant metrics, such as RAM consumption. For instance, kubectl top pod provides data on memory usage inside the pod, and jcmd GC.heap_info gives information about the heap usage. Use Native Memory Tracking in addition to GC logs to understand how much memory your application actually uses. Take Kubernetes overhead into account. The pod overhead is the resources it uses when running on a node. For instance, the Fargate pod adds 256 MB to each pod’s memory reservation for necessary K8s components. Improper configuration will lead to Overutilization, when the service consumes all available memory, the application constantly performs garbage collection, and we add more instances to resume normal operation; Underutilization, when the application doesn’t use all available memory, and we have to spin up more instances and waste resources. Distinguish types of services When performing optimizations, always consider the type of service. Some services are less critical, and some are highly critical, so the performance requirements differ. Less critical services Have moderate RTO (recovery time objectives) requirements; Have a Burstable Quality of Service (QoS) class, meaning that the pods have lower resource guarantees based on the container request and don’t require a specific memory limit (but at least one container in the pod must have a memory or CPU request/limit); Can be used with spot instances, which are up to 90 % cheaper than on-demand instances and run when the capacity is available. Highly critical services Have strict RTO requirements; Are highly elastic and designed for handling exponential growth; Have a Guaranteed QoS class, meaning that the pods have strict resource limits and are guaranteed not to be killed until they exceed their limits. All containers in such pods must have a CPU limit/request and a memory limit/request. Therefore, the performance requirements should be determined per service. Note that Java applications don’t handle the vertical scaling well and are better suited for horizontal scaling. This means that the requests and limits should be based on the peak performance data. In addition, the scaling strategy shouldn’t be based on CPU and RAM metrics only. Sometimes latency or throughput are more important for the given application. Use Kubernetes probes effectively Kubernetes probes used by the kubelet are vital for gathering information about the health of your containers. On the other hand, their incorrect configuration may lead to performance degradation and unnecessary scaling. There are three types of Kubernetes probes: A startup probe determines if the containerized application has started. Other probes are turned off until the startup probe confirms the successful startup. A liveness probe determines whether the container is running. If not, it signals the kubelet to restart it. A readiness probe decides when the container is ready to accept network requests. The probes work better together. For instance, a startup probe is perfect for slow-starting containers because otherwise, the liveness probe could kill the container prematurely unless the timeoutSeconds is appropriately set. The readiness probe could say that the container is running all right when, in reality, the application is in a deadlock, which can be identified only by the liveness probe. Major Java frameworks, including Spring Boot, support Kubernetes probes configuration and autoconfiguration. With Spring Boot, you need to add the spring-boot-starter-actuator dependency to the pom.xml file. Spring Boot will register liveness and readiness probes automatically when the management.health.probes.enabled property is set to true (or management.endpoint.health.probes.enabled=true starting with Spring 2.3.2) in application.properties. You can then adjust the probe settings for your workloads. Proper configuration will enable you to avoid frequent and unnecessary container restarts or other issues. For instance, suppose the probes don’t wait long enough (e.g., when you set the response time limits too low) and return negative responses. In that case, the Kubernetes autoscaler may decide that additional pods are needed and perform unrequired vertical scaling, thus wasting resources. Upgrade the Java version Even if you use regularly updated JDK images, it’s not time to bask in the sun yet. Upgrading the Java version is no less important because the overall JVM performance is getting better with each JDK release. For example, Java has become increasingly container-aware starting with JDK 9: JDK versions 10+ have better Docker container detection algorithm and allow for better resource configuration usage and more flexible adjustment of heap percentage with available RAM; Versions 11+ collect and use cgroups v1 data; Versions 17+ and 11u have cgroups v2 support; And so on. In addition, fresh versions include numerous improvements to garbage collection, affecting the KPIs greatly. But although some fixes are backported to legacy Java versions, fewer and fewer improvements make it to older LTS versions with each new release. Therefore, upgrading the JDK is crucial for optimal Java performance, not only in Kubernetes. But what if you can’t migrate to a newer Java version now? After all, the migration requires solving the compatibility issues and sometimes rewriting the code significantly. You can inject the power of JVM 17 into your JDK 11-based projects with Liberica JDK Performance Edition. Boost the startup, latency, and throughput immediately with little to no code adjustments, and migrate at your own pace! Select the appropriate Garbage Collector The Java platform offers a variety of garbage collectors tailored for specific workloads and aimed at improving relevant KPIs. For instance, ParallelGC is suitable for high-throughput applications; G1GC is aimed at reducing latency; ShenandoahGC (not included with Oracle Java, but shipped with OpenJDK distributions, including Liberica JDK) focuses on keeping pauses short even with large heaps. The goal is to select the appropriate collector for your Kubernetes cluster — in most cases, it will be sufficient for performance improvement and won’t require exquisite GC tuning. Furthermore, developers should avoid automatic SerialGC switching. Suppose you set the -Xmx parameter to 2 Gb or less and limit the application to less than two processors. In that case, SerialGC will switch automatically (even specifying another collector explicitly in the JVM settings won’t remedy the situation). SerialGC might be optimal for single CPU machines and applications running in extremely memory-tight environments but may lead to significant performance degradation in other use cases. Therefore, don’t set the limits too low or use the -XX:+AlwaysActAsServerClassMachine that prevents automatic SerialGC usage. Use a small base OS image Minimizing the size of containers is crucial for optimizing the resource consumption in your Kubernetes clusters and keeping the cloud costs under control. Although several techniques enable the developers to keep their containers neat and lean, the top-priority step is to choose a minimalistic base OS image. This way, you will immediately reduce the container size without laborious JVM memory configuration or stripping the unnecessary packages off the OS image. The best lightweight Linux distributions for the cloud are Alpine and Alpaquita Linux. Both have a base image size of less than 4MB (additional packages are easily installed with the APK tool). Still, Alpaquita, which is 100% Alpine-compatible, has several distinguishing features that make it perfect for enterprise Java development: Two libc implementations, optimized musl and glibc, for enhanced performance and seamless migration; Additional kernel hardening and regular updates for optimal security; Tools facilitating Java development and four mallocs for various Java workloads; LTS releases and 24/7 support directly from the BellSoft engineers. A bonus for Spring developers: we created Alpaquita Containers tailor-made for Spring Boot apps and aimed at reducing their RAM consumption by up to 30 %. Don’t take our word for it — head over to the product page, transfer your app into our container, run the tests, and tell us how much memory you saved! Reduce startup and warmup time Reducing application startup is critical when you use AWS Lambdas or similar services. It is also essential if your cloud provider charges you for CPU time. In addition, JVM warmup is associated with increased memory consumption, so you have to allocate more memory to your instances, which won’t be used later. There are several ways to reduce Java application startup, including AppCDS, AOT-compilation, and some completely novel solutions, such as the CRaC method! The topic is too extensive for this article, so we will make a deep dive into it in the following one dedicated to methods for reducing the startup and warmup times of your Java apps. - [CVE-2023-4911 (Looney Tunables): a critical vulnerability in glibc](https://bell-sw.com/blog/cve-2023-4911-looney-tunables-a-critical-vulnerability-in-glibc/): A buffer overflow was discovered in the dynamic loader (ld.so) of GNU C Library (glibc) while processing the GLIBC_TUNABLES environment variable. Find out more about the vulnerability and how to mitigate the risk of exploits. Description The CVE-2023-4911 (dubbed Looney Tunables) was introduced in glibc 2.34 in April 2021. As most Linux distributions use this C library implementation, the vulnerability affects a significant number of systems. So what exactly is the issue? The glibc dynamic loader is responsible for determining, finding, and loading the shared libraries that the program needs at runtime. The GLIBC_TUNABLES variables allow the developers to adjust the performance and behavior of a library at runtime without recompiling the library or application. As it turned out, maliciously crafted GLIBC_TUNABLES variables can cause buffer overflow and enable the attackers to run code with root-level privileges. Risk scope The vulnerability was assigned a 7.8 CVSS score (high severity) because the glibc dynamic loader has elevated privileges when a local user launches programs with a set-user-ID or set-group-ID permissions. Therefore, the attacker can get full root access to the system. Mitigation If you use a musl-based Linux distribution, such as Alpine or Alpaquita, your applications are unaffected. For those developers who use a glibc-based distro, we recommend updating the OS as soon as possible. BellSoft has already released a security patch for a glibc version of Alpaquita Linux — update the image now and stay safe! - [How to reduce Java application startup time](https://bell-sw.com/blog/how-to-reduce-java-application-startup-time/): Waiting five seconds for an application to load is bearable, but the irritation grows every time you repeat the process. Imagine loading an app 100+ times a day. Now, imagine your bank account is charged for every second of your expectation. Sounds like a nightmare? It becomes a reality when you deploy Java microservices to the cloud and have to restart them constantly. The situation worsens if you use services that charge you for the compute time you consume. This article looks into ways of cutting Java application startup time and reaching peak performance almost instantly. We hope that you find the solution most suitable for your project. Table of Contents Why you have problems with Java application startup How does a Java application start up? Warmup: code interpretation and optimization Ways to accelerate JVM startup and warmup Application Class Data Sharing (AppCDS) Ahead-of-time compilation (Native Image) Project Leyden CRaC Why you have problems with Java application startup When we say that Java applications start slowly, we mean several consecutive processes: JVM startup, application startup, and JVM warmup. Interestingly, the startup part is not the main culprit of our anguish. How does a Java application start up? First of all, the JVM service code and data get loaded and initialized. Then, the initialization of core classes follows. After that, the dependencies of the main class get initialized. The invocation of the main method drives further execution. The JVM startup takes a few milliseconds on modern hardware. After that, the application starts. The application classes are loaded and initialized, the dependencies are resolved, and the necessary resources are loaded. At this stage, application-specific initialization may also occur (such as the database connections and so on). The application startup takes longer than the JVM start: for microservices, this phase can last seconds or up to a minute. Taken together, JVM and application start yield the time to first operation. But JVM still has a lot to do before reaching the state of peak performance. Warmup: code interpretation and optimization During the warmup phase, JVM compiles and optimizes the code. It is a very resource-intense and lengthy process because JVM needs to execute, compile, and profile different versions of machine code to select the most performant one. As a rule, the better the resulting code, the more required optimizations. Code compilation and optimization take substantially longer than the actual startup and may last several minutes in case of complex applications. In addition, JIT compilation is associated with higher overhead because of the necessity to translate bytecode to machine code and significant memory consumption due to the JVM size plus compiled code. The worst thing is that the process begins from ground zero every time you start your program! It may lead to Higher cloud costs if your provider charges for every second of operation and Resource overutilization because you must allocate more memory to your application than it needs. So, what can we do to accelerate the startup and warmup? The JVM startup reduction won’t make any difference: there’s no point in trying to win a couple of milliseconds. The application startup can be optimized as we will see below, but the biggest effort should be given to reducing the warmup phase as it takes the longest. We want to reach peak performance in as little time as possible regardless of the complexity of our application. One option is to switch off the C2 compiler to skip lengthy performance optimizations. But we don’t recommend doing that: by winning several seconds, you will lose greatly in overall performance. Instead, consider the alternative techniques described below. Ways to accelerate JVM startup and warmup Application Class Data Sharing (AppCDS) Application Class Data Sharing (AppCDS) is the OpenJDK feature aimed at improving a Java application’s startup time and memory footprint by creating an archive of classes used by the application. When the application starts, the JVM needs to find, load, and initialize the necessary classes and then map them into an internal data structure. Depending on the number of required classes (which may be hundreds or thousands), the process may take quite some time. The AppCDS enables the developers to store this information in an archive, which will be used for further launches or even shared among multiple JVM instances. JEP:350 Dynamic CDS Archives introduced in JDK 13 aims to further enhance the developer experience with the feature by eliminating the need to do trial runs to create a class list. AppCDS allows for good startup reduction, up to 50% depending on the configuration (more on using AppCDS and performance gains in the article How to use CDS with Spring Boot). But if you need more drastic startup time reduction, consider using GraalVM Native Image or Coordinated Restore at Checkpoint described below. Ahead-of-time compilation (Native Image) The AOT compiler translates Java bytecode into OS-specific machine code, performs necessary optimizations, and eliminates unused code dependencies at the build stage. The resulting executable (native image) Starts up almost instantly (in 1/10 s) because there’s no need to interpret bytecode and search for hotspot; Reaches peak performance immediately without warmup because all performance optimizations are done before application execution; Doesn’t require JVM to run but includes the necessary runtime components (garbage collectors, thread scheduling, etc.); Provides lower overhead. Java developers wishing to integrate AOT compilation into their projects can use GraalVM Native Image, part of the GraalVM project. BellSoft also offers a GraalVM CE-based native-image compiler, Liberica Native Image Kit, recommended by Spring. In addition, the most popular Java frameworks offer baked-in support for Native Image, including Spring Boot 3. AOT seems to solve the startup and warmup problem, but migrating existing projects to Native Image can be pretty challenging. The AOT compilation happens under the closed-world assumption, meaning that the compiler has to know about all methods and dependencies used at runtime, or else they won’t make it to the native executable. As a result, the application will throw runtime errors or behave unexpectedly. Native Image does not support dynamic features such as Reflection, Serialization, JNI, so you have to either rewrite your application or provide relevant configuration at build time. We discussed some intricacies of development with Native Image in our previous article. To sum up, you must carefully evaluate the complexity and specifics of your application to determine whether migration to Native Image is worth the trouble. Project Leyden Project Leyden is the OpenJDK project under development, whose main objective is to improve Java applications’ startup time through static images. Project Leyden will rely on the existing JDK components, including the HotSpot JVM, the jlink tool, and the AppCDS. The project team works toward gradually adopting the full closed-world constraint, starting with less strict constraints and delivering incremental optimizations. This will enable more projects to benefit from static images. Project Leyden is a promising solution, but it is still under development. Early-acces builds are already available though, and you can experiment with them by following the guide in the release notes or our tutorial on Using Project with Spring Boot. So we want to preserve the power of the JVM JIT compiler, which can optimize performance on the fly but skip the lengthy warmup procedure. Could we warm up the application once, save this state, and pick up from where we left off for all the following starts? We can do that if we get our JVM hooked on CRaC! CRaC Coordinated Restore at Checkpoint (CRaC) is an OpenJDK project aimed at reducing startup and providing high performance immediately for Java applications. Based on the CRIU project for Linux with enhancements tailored to the specifics of the JVM process, CRaC offers the following functionality: You start your Java application the usual way and wait until it warms up and reaches stable performance. Then, you take a snapshot of the current JVM state at an arbitrary time (“checkpoint”) and save it to a set of files. When you start your application next time from this set of files (“restore”), you continue the operation from the moment the snapshot was taken, so the performance level will be the same. This way, the application starts almost immediately at a stable performance level without any warmup. At the same time, the JIT compiler is still there, ready for further performance optimization if required. That was the “Restore at Checkpoint” part, but what about “Coordinated”? Coordinated restore means that the application is aware that it is being checkpointed and restarted, so it can perform certain before-checkpoint and after-restore operations to ensure smooth operation. For instance, it will ensure that all files, connections, and sockets are closed, or the snapshot won’t be taken. In addition, it can react to changes in the environment. What is more, CRaC also enables the developers to optimize memory consumption in the cloud by starting the application in a test container, taking a snapshot, and then sending the application together with the snapshot to the production environment. If you want to learn more about the API, its benefits, and main considerations when using it, refer to the article What is CRaC. Popular Java frameworks have already started adopting the CRaC feature, which enables the developers to benefit from the functionality with little to no code adjustments. And Bellsoft released Liberica JDK 17 & 21 builds with CRaC support and ready-to-use containers with CRaC. Follow our guide on using CRaC with Java applicationa in a Docker container and start experimenting with this powerful feature! Conclusion To sum up, the reduction of Java startup and warmup time always comes at a cost. Although CRaC seems to be the most promising solution in this regard, developers have to familiarize themselves with it before integrating it in production. We are sure you have lots of questions about the CRaC feature, so we will delve into the technology in our following article. Subscribe to our newsletter and don’t miss it! - [Liberica JDK 8u392, 11.0.21, 17.0.9, and 21.0.1 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u392-11-0-21-17-0-9-and-21-0-1-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u401, 7u401, 8u391, 11.0.20.1.1, 17.0.8.1.1, and 21.0.0.0.1. CPU releases include patches for Common Vulnerabilities and Exposures (CVE). In addition, we release PSU versions 8u392, 11.0.21, 17.0.9, and 21.0.1 with non-critical fixes and general improvements. The release contains 837 fixes and backports overall. BellSoft participated in eliminating 11 issues in all releases. Table of Contents How to keep your runtime secure The summary of fixes List of security issues fixed Summary of fixes in Liberica JDK Supported platforms Enjoy the most stable runtime! How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 3 security issues (CVEs) fixed 58 total security fixes in CPU release: in Liberica 6u401: 12 security fixes; in Liberica 7u401: 11 security fixes; in Liberica 8u391: 13 security fixes; in Liberica 11.0.20.1.1: 7 security fixes; in Liberica 17.0.8.1.1: 8 security fixes; in Liberica 21.0.0.0.1: 7 security fixes. In addition, PSU releases include a total of 779 bugs and backports fixed: in Liberica 8u392: 14 security fixes + 22 additional fixes (+ 8 in FX); in Liberica 11.0.21: 8 security fixes + 184 additional fixes (+ 16 in FX); in Liberica 17.0.9: 9 security fixes + 364 additional fixes (+ 26 in FX); in Liberica 21.0.1: 8 security fixes + 110 additional fixes (+ 10 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-22067 5.3 other-libs corba network low none none unchanged none low none CVE-2023-22081 5.3 security-libs javax.net.ssl network low none none unchanged none none low CVE-2023-22025 3.7 hotspot compiler network high none none unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 CVE-2023-22067 • CVE-2023-22081 • • • • CVE-2023-22025 • • Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Meet BellSoft at KubeCon 2023](https://bell-sw.com/blog/meet-bellsoft-at-kubecon-2023/): BellSoft is excited to be a silver sponsor of KubeCon + CloudNativeCon North America 2023, scheduled for 6–9 November, a flagship conference for cloud native adopters and technologists organized by the Cloud Native Computing Foundation (CNCF). The CNCF is part of the Linux Foundation, under whose auspices developers and organizations advance and manage open-source projects. In line with the Linux Foundation endeavors, the CNCF strives to make cloud native technologies ubiquitous and sustainable and provides support and guidance for the leading open-source solutions in cloud native such as Kubernetes, Prometheus, Cloud Native Buildpacks, and many others. As one of the biggest graduated CNCF projects, Kubernetes is a major topic at KubeCon, with numerous presentations dedicated to the cutting-edge practices of working with this technology. BellSoft is proud to be a member of the Linux Foundation and the CNCF and contributes actively to enriching the cloud native ecosystem. We are committed to open source development, and most of our products are free. Since 2018, we have focused on enhancing the Java platform for the cloud environment and providing organizations and developers with secure and performant solutions for cloud native Java development: We ported Alpine Linux to OpenJDK so developers can build miniature and fast containers for their Java workloads. Our Liberica JDK is the default runtime in Paketo buildpacks that implement the Cloud Native Buildbacks specification and help accelerate the deployment of cloud native apps. We created Liberica JDK Lite, a unique Java runtime optimized for cloud instances with a minimal footprint. We developed Alpaquita Linux, inspired by Alpine, but boasting enhanced performance, flexibility, and performance. The pinnacle of our efforts, Alpaquita Container, is tailored to cloud native Java microservices and helps developers to reduce memory footprint and increase the overall performance of their applications. Enterprise customers can build a secure and reliable Java-based cloud infrastructure, receiving 24/7 support and LTS releases for Linux and JDK from one vendor. We look forward to four days at KubeCon that will encompass dozens of exciting presentations from industry leaders on best practices and innovations in cloud native. In addition, the conference provides excellent opportunities for networking as it gathers globally renowned experts, representatives of leading tech organizations, contributors to the cloud evolution, and developers working with the cloud. Come and meet us in person at booth B29! We will be happy to discuss the latest advances in cloud native technologies, including security in the cloud, performant microcontainers, and native images, and share our insights on making Java a long-term resident in the cloud. See you at the conference! - [Migration to Spring Boot 3](https://bell-sw.com/blog/migration-to-spring-boot-3/): The migration to Spring Boot 3 boasting numerous enhancements, including baked-in support for GraalVM Native Image, is in full swing! If you are just embarking on this journey, below is a guide how to painlessly upgrade the version of your favorite framework. Please note that the gradual migration is recommended, so make sure you switch to Spring Boot 2.7 before proceeding. Table of Contents Upgrade to Java 17 Check for deprecated code Core changes Configuration properties Jakarta EE Logging date format Auto-configuration files Web applications changes Trailing slash matching Apache HttpClient Pattern parsing Actuator changes “httptrace” renamed Endpoints sanitization Actuator JSON GraalVM Native Image support Spring Security changes ReactiveUserDetailsService SAML2 Data access changes Hibernate Elasticsearch MySQL JDBC Driver New Alpaquita Containers to slash RAM consumption Upgrade to Java 17 Spring Boot 3.0 requires JDK 17 and Spring Framework 6.0, so you have to upgrade your Java version first. If you haven’t done this yet, our instructions on moving the workloads from JDK 8 or JDK 11 to JDK 17 will guide you through the process. In addition, you need to upgrade many dependencies. Here’s a list of versions compatible with Spring Boot 3.0, but you should also keep track of libraries not managed by Spring Boot directly. You should also verify that third-party libraries you use are compatible with Spring Boot 3.0 and make relevant changes. Check for deprecated code According to the Spring Boot Deprecations policy, methods, constructors, or classes marked as @deprecated are removed from the framework in two minor releases, so if you are migrating from Spring Boot 2.7, verify that your code doesn’t utilize deprecated functionality. To find calls to deprecated features easily, you can use a -Werror option that treats all warnings as errors and fails the build. Core changes Spring Boot 3.0 introduces several changes to its core functionality. Configuration properties Some configuration properties were renamed or removed, for example: spring.data.cassandra. was moved to spring.cassandra. spring.redis. was moved to spring.data.redis. management.metrics.export. was moved to management..metrics.export server.max-http-header-size was deprecated in favor of a new server.max-http-request-header-size Therefore, you need to update your application.properties or application.yml file. You can use the spring-boot-properties-migrator dependency, which identifies such properties and temporarily migrate them at runtime. Jakarta EE Spring Boot uses Jakarta EE 10, so other technologies were upgraded as well for compatibility sake. For instance, Spring Boot 3.0 uses Servlet 6.0 and JPA 3.1. You need to update your pom.xml file accordingly. Furthermore, Jakarta EE 10 utilizes jakarta packages instead of javax, so you may have to fix your import statements. Logging date format The new default date and time format for the log messages is now yyyy-MM-dd’T’HH:mm:ss.SSSXXX, where T separates the date and time, and XXX stands for the timezone offset. The new format conforms to ISO-8601 standard, but if you want to use the previous format (yyyy-MM-dd HH:mm:ss.SSS), you can do that with the LOG_DATEFORMAT_PATTERN environment variable or logging.pattern.dateformat property. Auto-configuration files Spring Boot 3.0 introduces a new META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file, which substitutes the deprecated method of registering auto-configurations in spring.factories. Web applications changes Trailing slash matching The trailing slash matching configuration has been deprecated, and its default property is set to false. It means that in the case of a similar annotation @GetMapping("/home/hello"), the controller will match only on “GET /home/hello”, but "GET /home/hello/” will result in 404 error. Apache HttpClient The RestTemplate was upgraded to HttpClient 5. Apache HttpClient version 4 is no longer supported. The valid replacement is org.apache.httpcomponents.client5:httpclient5. Pattern parsing Spring MVC now uses PathPatternParser by default. Although it is possible to override this behavior manually, The Spring team recommends using PathPatternParser whenever possible due to its superior performance. Actuator changes Some essential updates introduced to the actuator module are as follows. “httptrace” renamed The httptrace endpoint was renamed to httpexchanges to avoid confusion with Micrometer Tracing parameters. Consequently, the related classes were also renamed, for instance, HttpTraceRepository turned into HttpExchangeRepository. Endpoints sanitization Spring masked the sensitive key values in /env and /configprops endpoints before, but the new framework version implements an even more secure approach. Now, all values are masked by default. You can use one of the following values to define whether the values are shown or not: NEVER — all values are masked (default); ALWAYS — all values are shown; WHEN_AUTHORIZED — values are shown if the user is authorized. Users are always considered to be authorized for JMX. In the case of HTTP, users are authorized if they are authenticated and have specified roles. Actuator JSON An isolated ObjectMapper instance is now used for responses from the actuator endpoints. In case you implement custom endpoints, make sure that responses implement the OperationResponseBody interface to take ObjectMapper into consideration when serializing responses as JSON. GraalVM Native Image support One of the most exciting features of Spring Boot 3.0 is the out-of-the-box support for GraalVM Native Image! Developers can now turn their Spring Boot apps into performant native images with an almost instant startup using Maven or Gradle plugins without special configuration, taking advantage of Cloud Native buildpacks or Native Build tools. In the latter case, you need to install a native-image compiler. The Spring team recommends Liberica Native Image Kit (NIK) for that purpose, which is also used as the default native-image builder in Paketo buildpacks. We prepared a guide on working with native images for Spring Boot developers — you will find here a step-by-step instruction to integrating Native Image into your project as well as tips and recommendations on overcoming possible compatibility challenges. Spring Security changes Spring Boot 3.0 supports Spring security 6.0. It is recommended to migrate to Spring Security 5.8 first, and then complete the transition to version 6.0. Necessary code adjustments depend on whether you have a Servlet or Reactive application, but some of the most significant changes are described below. ReactiveUserDetailsService A ReactiveUserDetailsService is not auto-configured anymore if the AuthenticationManagerResolver is present. SAML2 The spring.security.saml2.relyingparty.registration.{id}.identity-provider properties are no longer supported, so developers should use new spring.security.saml2.relyingparty.registration.{id}.asserting-party ones. Data access changes Hibernate Spring Boot 3.0 supports Hibernate 6.1 by default, so if you have a separate dependency for Hibernate, you need to update it. The version 6.1 encompasses two major changes related to basic array/collection mappings and enum mapping, but Hibernate 6.0 includes numerous updates that may affect your application, so if you use older Hibernate versions, consult the Hibernate 6.0 Migration Guide first. Elasticsearch Elasticsearch’s high-level REST client is no longer supported. Instead, Spring Boot supports auto-configuration of other Elasticsearch clients: low-level REST client, Java API client, and ReactiveElasticsearchClient provided by Spring Data Elasticsearch. In addition, ReactiveElasticsearchRestClientAutoConfiguration was renamed to ReactiveElasticsearchClientAutoConfiguration and moved to org.springframework.boot.autoconfigure.elasticsearch. MySQL JDBC Driver The MySQL JDBC Driver is now referred to as com.mysql:mysql-connector-j instead of mysql:mysql-connector-java. New Alpaquita Containers to slash RAM consumption While you are upgrading your Spring Boot, consider breathing new life into your containers as well! Alpaquita Containers are tailored to increasing the performance of your containerized Spring Boot apps. Based on a lightweight Alpaquita Linux with two libc options and four mallocs for various Java workloads and Liberica JDK Lite, a Liberica JDK flavor for cloud deployments, They will help you instantly reduce the RAM consumption of containers by up to 30 %. Test them out and see for yourself! - [Liberica Native Image Kit 22.3.4, 23.0.2, 23.1.1 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-22-3-4-23-0-2-23-1-1-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 22.3.4, 23.0.2, 23.1.1 for JDK 11.0.10, 17.0.9, and 21.0.1 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-22067 5.3 other-libs corba network low none none unchanged none low none CVE-2023-22081 5.3 security-libs javax.net.ssl network low none none unchanged none none low CVE-2023-22025 3.7 hotspot compiler network high none none unchanged none low none Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [How to deploy Spring Boot application on Kubernetes](https://bell-sw.com/blog/how-to-deploy-spring-boot-application-on-kubernetes/): If you want to own the cloud, you have to own Kubernetes, which is the most popular container orchestration system that dramatically facilitates the management of cloud-native applications. This article will guide you through containerizing and deploying a Spring Boot app on the Kubernetes cluster. The code is available on GitHub. By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can reduce the memory footprint of your containers by up to 30 %! There containers also support CRaC API, so you can easily reduce startup and warmup times of your Spring Boot services from minutes to milliseconds by using Java with CRaC. Give it a try! Table of Contents Prerequisites Set up the environment Create a Spring Boot application Build a Docker container image Write a Dockerfile Containerize the app Push the image to the container registry Deploy the application on Kubernetes Create Kubernetes deployment file Create a Kubernetes service file Deploy the container image to Kubernetes Access the application Conclusion Prerequisites JDK 17 (I will use Liberica JDK recommended by the Spring team) Docker Maven Your favorite IDE (I will use IntelliJ IDEA) Set up the environment First of all, we need to set up a local Kubernetes cluster. Local clusters are great if you need to master the technology and test application performance without the risk of breaking the production environment. For our purposes, we will use minikube, a widespread, easy-to-install Kubernetes distribution. To set up a single-node K8s cluster, refer to our previous guide. Create a Spring Boot application Head to Spring Initializr to create a simple Spring Boot application. Select Java, Maven, the latest stable version of Spring Boot 3 (I have 3.1.5; yours may be different), and Jar packaging. The name of our project will be spring-boot-app. We will also need two dependencies, Web and Actuator; the latter supports endpoints for convenient application monitoring. Creating a Spring Boot application Click “Generate,” unzip the file, and open the application in your IDE. Configure the application as shown below: @SpringBootApplication @RestController public class SpringBootAppApplication { @RequestMapping("/") public String home() { return "Hello Kubernetes!"; } public static void main(String[] args) { SpringApplication.run(SpringBootAppApplication.class, args); } } Finally, build a runnable jar with $ mvn package Build a Docker container image Write a Dockerfile To dockerize our Spring Boot application, we need to write a Dockerfile. Although the fastest way to build a container is to use buildpacks (such as Paketo buildpacks), good old Dockerfiles provide fine-grain control of our deployment. For example, you can select a base image, such as Alpaquita Container, tailor-made for Spring Boot apps. Based on a lightweight Alpaquita Linux and Liberica JDK Lite, it allows the developers to build performant microcontainers and save up to 30 % RAM! Add the following file to the root directory of your application: FROM bellsoft/liberica-runtime-container:jre-17-stream-musl COPY target/spring-boot-app-0.0.1-SNAPSHOT.jar springbootapp.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/springbootapp.jar"] Here, we import a base image with musl-based Alpaquita (a glibc option is also available) from BellSoft’s repository, copy the jar file (spring-boot-app-0.0.1-SNAPSHOT.jar) we generated in the previous section from the target folder into the root folder of the container and name it springbootapp.jar, indicate the port for our web application, run the springbootapp.jar inside the container with java -jar command. Containerize the app Build the image with $ docker build . -t spring-boot-app You can run the container with $ docker run -p 8080:8080 spring-boot-app To verify that it is running, run the following command in another Terminal window: $ curl localhost:8080/actuator/health The answer should be {"status":"UP"} Stop the container and proceed to the next step. Push the image to the container registry Before deploying the application on Kubernetes, you need to publish it on a container registry because this is where Kubernetes pulls the images. We will use the Docker Hub Container Registry, so you must create a Docker ID if you don’t have one yet. After that, run the following commands (replace the with your Docker Hub username): $ docker tag spring-boot-app /spring-boot-app $ docker push /spring-boot-app Deploy the application on Kubernetes Create Kubernetes deployment file Create a deployment.yaml file with a following content and place it to the root directory of your project: apiVersion: apps/v1 kind: Deployment metadata: name: spring-boot-app spec: replicas: 2 selector: matchLabels: app: spring-boot-app template: metadata: labels: app: spring-boot-app spec: containers: - name: spring-boot-app image: /spring-boot-app ports: - containerPort: 8080 Let’s look at the file closely. What exactly do we tell Kubernetes to do? First of all, we define the type of our resource, which is Deployment, its version (apps/v1) and name (spring-boot-app). After that, we specify how many replicas of our container we want to have (here, two). Containers in Kubernetes run on Pods, the smallest deployable units that run one or several containers with shared network resources, storage, and a specification on how to run containers. Our Deployment file creates a ReplicaSet with two replicated Pods. The metadata-labels part specifies the label for the Pods (app: spring-boot-app). This label will be used by the selector to define how the ReplicaSet finds the Pods (the matchLabels selects the Pods with the app: spring-boot-app label). Finally, we provide the information about the container we want to run by specifying its name (spring-boot-app), the name of the image we published to Docker Hub (/spring-boot-app), and the port the container listens on (8080). Create a Kubernetes service file We defined how to run our containerized application in the Kubernetes cluster, but we need to make it accessible from the outside of the cluster. For that purpose, we will create a Service by means of a service.yaml file: apiVersion: v1 kind: Service metadata: name: spring-boot-app spec: type: LoadBalancer selector: app: spring-boot-app ports: - protocol: TCP port: 80 targetPort: 8080 Here, we define a selector, which selects the Pods to be exposed based on the provided label (app: spring-boot-app). The type of our Service is LoadBalancer, which tracks the availability of Pods and distributes network traffic among them. If the Pod is unavailable for some reason, Load Balancer doesn’t route traffic to it and looks for available Pods, ensuring continuous availability of the application. Note that if we don’t specify the type of our Service, Kubernetes will assign it the default ClusterIP type, which makes Pods accessible from within the cluster, but not from the outside. Finally, we define that our Service listens on port 80 and routes the traffic to port 8080. Deploy the container image to Kubernetes Start minikube with minikube start if you haven’t done it yet. Deploy both files to Kubernetes with the commands: $ kubectl apply -f deployment.yaml $ kubectl apply -f service.yaml You will get the following output: deployment.apps/spring-boot-app created service/spring-boot-app created Let’s make sure that our application is running. Execute $ kubectl get all You will get a similar output: NAME READY STATUS RESTARTS AGE pod/spring-boot-app-dbf645dbf-f59ck 1/1 Running 0 5m7s pod/spring-boot-app-dbf645dbf-qkpw9 1/1 Running 0 5m7s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 443/TCP 20h service/spring-boot-app LoadBalancer 10.102.77.63 80:30936/TCP 5m7s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/spring-boot-app 2/2 2 2 5m7s NAME DESIRED CURRENT READY AGE replicaset.apps/spring-boot-app-dbf645dbf 2 2 2 5m7s As we specified two replicas in our Deployment file, you will see them both as pod replicas with different names. You may have to repeat kubectl get all until the STATUS changes to Running. Access the application Our application is running successfully, but we can’t access it from the outside yet. You can expose your service with: $ minikube service spring-boot-app --url This way, minikube will generate the URL where you can access your application and see our “Hello Kubernetes!” message. Alternatively, you can create a routable IP with $ minikube tunnel After that, run $ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 443/TCP 21h spring-boot-app LoadBalancer 10.102.77.63 127.0.0.1 80:30936/TCP 39m You can see that our LoadBalancer deployment was assigned an external IP. Let’s access our application at :80 $ curl 127.0.0.1:80/actuator/health {"status":"UP","groups":["liveness","readiness"]} Congratulations! You successfully deployed your Spring Boot application on Kubernetes! Conclusion In this article, we learned how to containerize a Spring Boot application and deploy it on the Kubernetes cluster. From here on, you can experiment with your deployment — scale the application, monitor its behavior, and tune the performance. So the essential step is done, but there are a lot more exciting things to accomplish! Subscribe to our newsletter if you want to read more guides on making your Spring Boot app feel at home in the cloud. - [BellSoft releases Liberica JDK LTS 17 and 21 with Coordinated Restore at Checkpoint (CRaC) for fast startup](https://bell-sw.com/blog/bellsoft-releases-liberica-jdk-lts-17-and-21-with-coordinated-restore-at-checkpoint-crac-for-fast-startup/): We are happy to announce the feature release of Liberica JDK LTS versions 17 and 21 with Coordinated Restore at Checkpoint (CRaC) support, enabling the developers to make running application snapshots and reduce the startup and warmup time of Java application to mere milliseconds! As opposed to other methods of reducing startup and warmup time, such as Application Class Data Sharing (AppCDS) and AOT compilation, CRaC helps to achieve instant startup at peak performance without losing the ability to further optimize performance with JIT compiler. The new builds will be available for x86_64 и AArch64 CPU architectures and Linux operating system. Before diving into experiments with JVM with CRaC, let’s discuss the functionality in more detail to get a clearer picture of its capabilities and specifics. Table of Contents What is Coordinated Restore at Checkpoint (CRaC)? Coordinated processes for enhanced reliability CRIU vs CRaC Spring Boot and Liberica JDK with CRaC — made for each other! Which applications need CRaC Main considerations when using CRaC Download the new Liberica JDK builds with CRaC and start experimenting! What is Coordinated Restore at Checkpoint (CRaC)? CRaC is an OpenJDK API that enables the developers to pause a running Java application (perform a checkpoint), save this state to a file, and then restore the application from the file from the moment it was paused. Metaphorically speaking, the process is similar to clicking "Save" in a computer game and then resuming playing with all your game achievements intact. To learn more about the feature and main considerations when using it, as well as which applications will benefit from the API, refer to our article What is CRaC. Spring Boot and Liberica JDK with CRaC — made for each other! Spring Boot currently integrates with CRaC as a Proof-of-Concept, the in-baked support is planned in future releases. Thanks to CRaC support, Liberica JDK, the default runtime for Spring, will help developers on their journey towards smooth integration of the functionality into their Spring Boot projects so that they can enjoy unprecedented startup and warmup speed with minimal code rewriting. We already tested CRaC-ed Liberica JDK with Spring Boot Petclinic, and the results are amazing! Time to first operation reduced from 7.1 seconds to 54 milliseconds without additional configuration! Experimental setup: CPU: Intel(R) Xeon(R) CPU X5675 @ 3.07GHz, JDK: Liberica JDK 17.0.8, OS: Linux Ubuntu 22.04.1. Spring Boot Petclinic and Liberica JDK with CRaC: startup study results Curious to know how to use CRaC with Java apps and Spring Boot projects? Head to our tutorial with step-by-step instructions! Download the new Liberica JDK builds with CRaC and start experimenting! CRaC is an open-source OpenJDK project, so Liberica JDK with CRaC is free for use. Liberica JDK with CRaC support can currently be used on bare metal and in virtual machines. Support for containerized workloads is in the makings. Please note that OpenJDK builds with CRaC, including Liberica JDK, are not yet verified by TCK. So you can experiment with the functionality, see how it fits into your workloads and how it benefits your project. The information about new Liberica JDK builds with CRaC support will also be available via Discovery API. To get started with the feature, you can visit the official GitHub CRaC repository or read the online documentation on the OpenJDK page of the project, which includes hardware and software requirements, guides on configuring the JDK, troubleshooting tips, and so on. You can also consult BellSoft engineers, and we will be happy to help! Download Liberica JDK with CRaC - [How to use CRaC with Java applications](https://bell-sw.com/blog/how-to-use-crac-with-java-applications/): Liberica JDK with Coordinated Restore at Checkpoint (CRaC) support will enable the developers to bring their applications to a new level, with startup and warmup times reduced to mere milliseconds! But like with any new technology, you need to familiarize yourself with CRaC before integrating it into enterprise development. This article provides a step-by-step tutorial on using CRaC with Java projects, including Spring Boot applications. Table of Contents Prerequisites Simple Java application Solving the possible issues Reference Spring Boot application Prerequisites Liberica JDK 17 with CRaC support (Although CRaC support is available for Liberica JDK 17 and 21, in this article, we assume JDK 17 with CRaC to be your default JDK). You can download the Java builds with CRaC here. Spring Boot 3.2 that supports CRaC Your favorite IDE Note that with deb and rpm packages, CRaC will work out of the box because they grant the necessary access rights to the criu executable under the hood. In the case of tar.gz packages, we have to grant these rights manually. If you downloaded the tar.gz file, run the following commands for Liberica JDK 17.0.9 with CRaC: $ sudo chown root:root jdk-17.0.9-crac/lib/criu $ sudo chmod u+s jdk-17.0.9-crac/lib/criu Check the owner and permissions of the criu executable: $ ls -al jdk-17.0.9-crac/lib/criu $ -rwsrwxr-x 1 root root 8478552 Oct 30 17:31 jdk-17.0.9-crac/lib/criu Simple Java application Create a simple Java application as follows: public class Example { public static void main(String args[]) throws InterruptedException { // This is a part of the saved state long startTime = System.currentTimeMillis(); for(int counter: IntStream.range(1, 100).toArray()) { Thread.sleep(1000); long currentTime = System.currentTimeMillis(); System.out.println("Counter: " + counter + "(passed " + (currentTime-startTime) + " ms)"); startTime = currentTime; } } } Note that as CRaC preserves the exact state of the running application, the snapshot may contain secrets or other kinds of sensitive information, so you should consider and eliminate possible security risks when working with this feature. Verify that you are using Liberica JDK with CRaC by checking the Java version, and then compile and start the application with CRaC support: $ java --version openjdk version "17.0.9" 2023-10-17 LTS OpenJDK Runtime Environment (build 17.0.9+14-LTS) OpenJDK 64-Bit Server VM (build 17.0.9+14-LTS, mixed mode, sharing) $ javac Example.java $ java -XX:CRaCCheckpointTo=checkpoint-dir Example The -XX:CRaCCheckpointTo=checkpoint-dir option points to the directory where the JVM data will be stored upon the checkpoint. Make a checkpoint with jcmd: $ jcmd Example JDK.checkpoint 86221: CR: Checkpoint ... The application console output: Counter: 1(passed 1007 ms) Counter: 2(passed 1010 ms) Counter: 3(passed 1001 ms) Counter: 4(passed 1000 ms) Counter: 5(passed 1000 ms) Counter: 6(passed 1000 ms) Nov 01, 2023 5:05:36 PM jdk.internal.crac.LoggerContainer info INFO: Starting checkpoint Killed Here comes the most interesting part — let’s try to restore the application. Run the following command (please note that only the -XX:CRaCRestoreFrom option is passed as the java argument): $ java -XX:CRaCRestoreFrom=checkpoint-dir Output: $ java -XX:CRaCRestoreFrom=checkpoint-dir Output: Counter: 7(passed 91124 ms) Counter: 8(passed 1000 ms) Counter: 9(passed 1001 ms) Counter: 10(passed 1000 ms) Counter: 11(passed 1000 ms) Counter: 12(passed 1001 ms) Counter: 13(passed 1000 ms) Counter: 14(passed 1000 ms) The passed time in milliseconds in line Counter: 7(passed 91124 ms) shows that the application was interrupted and then restored. Solving the possible issues To demonstrate the potential issues, let’s write another application: import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ExampleWithCRaC { private ScheduledExecutorService executor; private long startTime = System.currentTimeMillis(); private int counter = 0; public static void main(String args[]) throws InterruptedException { ExampleWithCRaC exampleWithCRaC = new ExampleWithCRaC().startTask(); } private void startTask() throws InterruptedException { executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(() -> { long currentTime = System.currentTimeMillis(); System.out.println("Counter: " + counter + "(passed " + (currentTime-startTime) + " ms)"); startTime = currentTime; counter++; }, 1, 1, TimeUnit.SECONDS); Thread.sleep(1000*30); executor.shutdown(); } } Start the application and make a checkpoint: $ java -XX:CRaCCheckpointTo=checkpoint-dir ExampleWithCRaC $ jcmd ExampleWithCRaC JDK.checkpoint 96828: CR: Checkpoint ... The output will be: Counter: 0(passed 1007 ms) Counter: 1(passed 999 ms) Counter: 2(passed 1000 ms) Counter: 3(passed 1000 ms) Counter: 4(passed 1000 ms) Counter: 5(passed 1000 ms) Counter: 6(passed 1000 ms) Counter: 7(passed 1000 ms) Counter: 8(passed 1000 ms) Counter: 9(passed 1000 ms) Counter: 10(passed 1000 ms) Nov 01, 2023 5:41:54 PM jdk.internal.crac.LoggerContainer info INFO: Starting checkpoint Killed Now, let’s try to restore the application: $ java -XX:CRaCRestoreFrom=checkpoint-dir Counter: 11(passed 64673 ms) The application started and then finished unexpectedly — we expected to get some iterations of the counter, but instead, more than 30 seconds passed, and the app finished. So the application should handle the checkpoint event. This can be achieved with classes from the jdk.crac package shipped with Liberica JDK with CRaC. Let’s modify our example and restart the counter threads. The jdk.crac.Resource interface allows us to handle the checkpoint and restore events by implementing the beforeCheckpoint and afterRestore methods. import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import jdk.crac.Context; import jdk.crac.Core; import jdk.crac.Resource; public class ExampleWithCRaCRestore { private ScheduledExecutorService executor; private long startTime = System.currentTimeMillis(); private int counter = 0; class ExampleWithCRaCRestoreResource implements Resource { @Override public void beforeCheckpoint(Context context) throws Exception { executor.shutdown(); System.out.println("Handle checkpoint"); } @Override public void afterRestore(Context context) throws Exception { System.out.println(this.getClass().getName() + " restore."); ExampleWithCRaCRestore.this.startTask(); } } public static void main(String args[]) throws InterruptedException { ExampleWithCRaCRestore exampleWithCRaC = new ExampleWithCRaCRestore(); Core.getGlobalContext().register(exampleWithCRaC.new ExampleWithCRaCRestoreResource()); exampleWithCRaC.startTask(); } private void startTask() throws InterruptedException { executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(() -> { long currentTimeMillis = System.currentTimeMillis(); System.out.println("Counter: " + counter + "(passed " + (currentTimeMillis-startTime) + " ms)"); startTime = currentTimeMillis; counter++; }, 1, 1, TimeUnit.SECONDS); Thread.sleep(1000*30); executor.shutdown(); } } Let’s try one more time. Compile the application and make a checkpoint: $ javac ExampleWithCRaCRestore.java $ java -XX:CRaCCheckpointTo=checkpoint-dir ExampleWithCRaCRestore Counter: 0(passed 1007 ms) Counter: 1(passed 999 ms) Counter: 2(passed 1000 ms) Counter: 3(passed 1000 ms) Counter: 4(passed 1000 ms) Counter: 5(passed 1000 ms) Counter: 6(passed 1000 ms) Nov 01, 2023 5:54:04 PM jdk.internal.crac.LoggerContainer info INFO: Starting checkpoint Handle checkpoint Killed Now, run $ jcmd ExampleWithCRaCRestore JDK.checkpoint 100389: CR: Checkpoint ... Finally, let’s restore our application: $ java -XX:CRaCRestoreFrom=checkpoint-dir ExampleWithCRaCRestore$ExampleWithCRaCRestoreResource restore. Counter: 7(passed 61407 ms) Counter: 8(passed 1000 ms) Counter: 9(passed 1000 ms) Counter: 10(passed 1000 ms) Counter: 11(passed 1000 ms) The issue is solved! Reference Spring Boot application First of all, let's try to use CRaC with the previous Spring Boot version. We will use the Spring Boot Petclinic application as an example. The source code is available here. After pulling the project, build the jar file with mvn package and use the following command: $ java -XX:CRaCCheckpointTo=cr -jar ./target/spring-petclinic-3.2.0-SNAPSHOT.jar |\ _,,,--,,_ /,`.-'`' ._ \-;;,_ _______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______ | | '---''(_/._)-'(_\_) | | | | | | | | | | _ | ___|_ _| | | | | |_| | | | __ _ _ | |_| | |___ | | | | | | | | | | \ \ \ \ | ___| ___| | | | _| |___| | _ | | _| \ \ \ \ | | | |___ | | | |_| | | | | | | |_ ) ) ) ) |___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / / ==================================================================/_/_/_/ :: Built with Spring Boot :: 3.2.0 2023-11-24T02:24:08.205-08:00 INFO 17053 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting PetClinicApplication v3.2.0-SNAPSHOT using Java 21.0.1 with PID 17053 (.../target/spring-petclinic-3.2.0-SNAPSHOT.jar started ...) ... Checkpoint the running application: $ jcmd spring-petclinic JDK.checkpoint 17053: An exception during a checkpoint operation: jdk.crac.CheckpointException Suppressed: jdk.crac.impl.CheckpointOpenSocketException: sun.nio.ch.ServerSocketChannelImpl[/[0:0:0:0:0:0:0:0]:8080] at java.base/jdk.internal.crac.JDKSocketResourceBase.lambda$beforeCheckpoint$0(JDKSocketResourceBase.java:44) at java.base/jdk.crac.Core.checkpointRestore1(Core.java:174) at java.base/jdk.crac.Core.checkpointRestore(Core.java:299) at java.base/jdk.crac.Core.checkpointRestoreInternal(Core.java:312) As you can see, CRaC failed to perform a checkpoint due to jdk.crac.CheckpointException(jdk.crac.impl.CheckpointOpenSocketException). To use CRaC with new Spring Boot and Spring Framework we need to add dependency on org.crac/crac package to pom.xml. org.crac crac 1.4.0 Let's check the difference of our updated pom.xml: $ git diff diff --git a/pom.xml b/pom.xml index 287a08a..f403155 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,11 @@ + + org.crac + crac + 1.4.0 + org.springframework.boot Now, let's check the dependency and used version for Spring Framework using Maven dependency command: $ mvn dependency:tree | grep "spring-boot:jar" [INFO] | +- org.springframework.boot:spring-boot:jar:3.2.0:compile $ mvn dependency:tree | grep "spring-core:jar" [INFO] | +- org.springframework:spring-core:jar:6.1.1:compile As a result, Spring Boot 3.2 with Spring Framework 6.1.1 will be used for building the Petclinic app. Build it as usual with mvn clean package and start: java -XX:CRaCCheckpointTo=cr -jar ./target/spring-petclinic-3.2.0-SNAPSHOT.jar |\ _,,,--,,_ /,`.-'`' ._ \-;;,_ _______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______ | | '---''(_/._)-'(_\_) | | | | | | | | | | _ | ___|_ _| | | | | |_| | | | __ _ _ | |_| | |___ | | | | | | | | | | \ \ \ \ | ___| ___| | | | _| |___| | _ | | _| \ \ \ \ | | | |___ | | | |_| | | | | | | |_ ) ) ) ) |___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / / ==================================================================/_/_/_/ :: Built with Spring Boot :: 3.2.0 2023-11-24T02:28:52.989-08:00 INFO 17325 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting PetClinicApplication v3.2.0-SNAPSHOT using Java 21.0.1 with PID 17325 (.../target/spring-petclinic-3.2.0-SNAPSHOT.jar started ..) 2023-11-24T02:28:52.995-08:00 INFO 17325 --- [ main] o.s.s.petclinic.PetClinicApplication : No active profile set, falling back to 1 default profile: "default" 2023-11-24T02:28:53.948-08:00 INFO 17325 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2023-11-24T02:28:53.992-08:00 INFO 17325 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 37 ms. Found 2 JPA repository interfaces. 2023-11-24T02:28:54.678-08:00 INFO 17325 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2023-11-24T02:28:54.686-08:00 INFO 17325 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-11-24T02:28:54.686-08:00 INFO 17325 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.16] 2023-11-24T02:28:54.718-08:00 INFO 17325 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-11-24T02:28:54.719-08:00 INFO 17325 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1658 ms 2023-11-24T02:28:55.024-08:00 INFO 17325 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2023-11-24T02:28:55.184-08:00 INFO 17325 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:be9e0d83-4345-497c-84ce-2df90b5740bb user=SA 2023-11-24T02:28:55.185-08:00 INFO 17325 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2023-11-24T02:28:55.330-08:00 INFO 17325 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2023-11-24T02:28:55.374-08:00 INFO 17325 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.3.1.Final 2023-11-24T02:28:55.400-08:00 INFO 17325 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2023-11-24T02:28:55.550-08:00 INFO 17325 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2023-11-24T02:28:56.297-08:00 INFO 17325 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) 2023-11-24T02:28:56.299-08:00 INFO 17325 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2023-11-24T02:28:56.544-08:00 INFO 17325 --- [ main] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used. 2023-11-24T02:28:57.751-08:00 INFO 17325 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 13 endpoint(s) beneath base path '/actuator' 2023-11-24T02:28:57.837-08:00 INFO 17325 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' 2023-11-24T02:28:57.850-08:00 INFO 17325 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 5.204 seconds (process running for 5.568) Now, we can perform the checkpoint: $ jcmd spring-petclinic JDK.checkpoint 17325: CR: Checkpoint ... The application output is as follows: 2023-11-24T02:29:39.847-08:00 INFO 17325 --- [Attach Listener] jdk.crac : Starting checkpoint 2023-11-24T02:29:39.876-08:00 INFO 17325 --- [Attach Listener] o.s.b.j.HikariCheckpointRestoreLifecycle : Evicting Hikari connections Killed We can check that the cr directory is created and contains a lot of files: $ ls cr ... core-17415.img core-17419.img files.img mm-17325.img pstree.img tty-info.img ... core-17416.img cppath fs-17325.img pagemap-17325.img seccomp.img ... core-17417.img dump4.log ids-17325.img pages-1.img stats-dump ... core-17418.img fdinfo-2.img inventory.img perfdata timens-0.img These files represent the dumped HotSpot JVM memory with all the necessary information about restoring the application. Finally, let's restore the Petclinic app: $ java -XX:CRaCRestoreFrom=cr 2023-11-24T02:30:59.012-08:00 WARN 17325 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m33s725ms577µs950ns). 2023-11-24T02:30:59.015-08:00 INFO 17325 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor : Restarting Spring-managed lifecycle beans after JVM restore 2023-11-24T02:30:59.019-08:00 INFO 17325 --- [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' 2023-11-24T02:30:59.020-08:00 INFO 17325 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor : Spring-managed lifecycle restart completed (restored JVM running for 50 ms) You can verify that the application is running successfully on localhost:8080. As we can see in the log file, we now have extremely low startup (restoring) time, only 50 ms. Compared to the application start during the first trial run without CRaC, which took 5.2 seconds, we can see that the startup became 100 times faster! To conclude, CRaC integrates conveniently with Spring Boot applications, although more complex projects may require additional testing and troubleshooting. If you have any questions regarding this feature, feel free to reach out, and our engineers will be happy to help! Contact us - [BellSoft releases dedicated builds of Liberica JDK 17 and 21 with CRaC](https://bell-sw.com/news/bellsoft-releases-dedicated-builds-of-liberica-jdk-17-and-21-with-crac/): SAN JOSE, Calif., Nov. 6, 2023 /PRNewswire/ -- Slow Java application startup is still an area for improvement for the OpenJDK that is of interest to the community. Coordinated Restore at Checkpoint (CRaC) is an OpenJDK project aimed at reducing startup and warmup times and providing immediate high performance for Java applications. Spring is one of the most popular Java frameworks, thanks to its deep tech prospects, pre-announced adding CRaC support. This is some of the most anticipated news for Java developers. Spring Boot and Liberica JDK with CRaC: startup study results BellSoft is glad to inform you that Liberica JDK 17 and 21 builds enhanced with CRaC are now available for free download. With this progressive version of Liberica JDK, you immediately eliminate the slow startup and warmup issue. The new builds will be available for x86_64 и AArch64 CPU architectures and Linux operating system. CRaC principles and benefits CRaC offers a Checkpoint/Restore API to create an image of a running application at an arbitrary point in time ("checkpoint") and then start the image from the checkpoint file ("snapshot"), restoring the state of an application from the point when the checkpoint was made. Essentially, the application can be paused and restarted from the moment it was paused, and in addition, numerous replicas of this file can be distributed, which is especially relevant for multi-instance deployment. In addition, it can perform essential preliminary tasks, such as closing network connections and open file descriptors, then return to normal operation after being restored and react to possible changes in the environment since the checkpoint. Coordinated Checkpoint/Restore makes the application aware of the fact that it is being paused and restarted. This way, the application can cancel the checkpoint if it deems the moment unsuitable for saving the state (when it performs certain operations such as saving the user data, for example). Liberica JDK with newly added CRaC technology significantly reduces your cloud costs, all due to: faster Java start-up and warm-up; smaller CPU consumption; same SLA on fewer resources. Our tests of CRaC-ed Liberica JDK with Spring Boot Petclinic show the results of startup reduction from 7.1 seconds to 54 milliseconds without additional configuration! "In recent years, OpenJDK enhancements were mainly aimed at adapting Java to the cloud. The Graal VM Native Image technology was a success for several years. The Leyden project and the Coordinated Restore and Checkpoint (CRaC) bring another approach to reducing Java application start-up and increasing its performance in the cloud. BellSoft endeavors to deliver such solutions at early stages of adaptation, and we believe that release of Liberica JDK with CRaC technology is very important for the entire OpenJDK community and for Spring users in particular," said Alex Belokrylov, BellSoft's CEO. Download Liberica JDK 17 and 21 with CRaC now to keep up with the higher speed and safety demands of the cloud environment! About BellSoft BellSoft delivers the most complete Java experience with a more secure, reliable, and cost-effective approach to application development on any platform and in any environment. BellSoft is one of the leading contributors to the OpenJDK, and the only vendor that supports current LTS Java versions, legacy JDK 6 & 7 and Liberica NIK. Liberica JDK is the runtime of choice for VMware, Spring Framework, JetBrains, and millions of users worldwide. For more information, visit www.bell-sw.com - [Documenting REST API with Swagger in Spring Boot 3](https://bell-sw.com/blog/documenting-rest-api-with-swagger-in-spring-boot-3/): API documentation is an indispensable part of developing web applications. Luckily, there are solutions that make meticulous manual documentation crafting a thing of the past. This step-by-step tutorial will guide you through integrating Swagger (based on OpenAPI 3.0 specification) into a Spring Boot project. By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can help you save up to 30% RAM! And did you know that you can easily reduce startup and warmup times of your Spring Boot services from minutes to milliseconds by using Java with CRaC support? Give it a try! Table of Contents What is Swagger? Swagger vs OpenAPI Integrating Swagger into a Spring Boot project Prerequisites Create a sample REST API project Add springdoc-openapi dependency Generate API documentation Integrate Swagger UI Configure Swagger 3 in Spring Boot with annotations Add Swagger API description Bean validation @Tag annotation @Operation annotation @ApiResponses annotation @Parameter annotation Conclusion What is Swagger? Swagger is a set of tools that help developers to create, edit, and use API documentation according to the OpenAPI specification. With Swagger, it is no longer necessary to manually write lengthy API docs: the solution is capable of reading the structure of the API you defined in the annotations of your code and automatically converting it into API specification. What is more, Swagger provides a user interface that generates interactive API documentation that lets users test the API calls in the browser. Main Swagger components are: Swagger Editor for writing and editing API specs, Swagger UI for creating interactive API documentation, Swagger Codegen for generating server stubs or client libraries for your API. Swagger vs OpenAPI Both terms, Swagger and OpenAPI, are used in the context of API documentation, but they are not the same. OpenAPI is a standard specification for describing API, and Swagger helps to create API docs in line with this specification. For example, BellSoft uses REST Discovery API based on OpenAPI specification to provide metadata about its products (version, build number, architecture, features, etc.) By querying this information, users can get a comprehensive understanding of product characteristics. Integrating Swagger into a Spring Boot project Prerequisites JDK 21 or later (I will use Liberica JDK recommended by the Spring team) Maven Your favorite IDE (I will use IntelliJ IDEA) Create a sample REST API project Skip this step if you want to use your own project. You can follow along or implement the examples from the tutorial into your application accordingly. The code for the demo project used below is available on GitHub. Our demo Spring Boot application will expose REST APIs for managing employees. The Employee will have id, first name, and last name. Our APIs will allow for getting a list of all employees, getting one employee, adding a new employee, updating the existing employee, and deleting an employee according to the following schema: Method URL Action GET /employees Get a list of all employees GET /employees/{employeeId} Get one employee by id POST /employees Add an employee PUT /employees Update an employee DELETE /employees/{employeeId} Delete an employee Head to Spring Initializr to create a skeleton of your project. Select Java, Maven, define a project name (I have openapidemo), and choose Java 21. To save the effort and avoid winding up a local database, we will use the in-memory H2 database. We will also need Spring Web, Lombok, Spring Data JDBC, and DevTools dependencies. DevTools is a great time saver because you don’t have to restart the application every time you introduce changes, the recompilation is performed on the fly. Generate the project and open it in your IDE. Let’s keep the structure simple and yet close to production-like setup. First, we will need the basic class Employee: @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class Employee { @Id private int id; private String firstName; private String lastName; public Employee(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } Let's also create DTOs so as not to pass around real database objects. For that, we will use a Mapstruct library. Here's the pom.xml with the Mapstruct dependency that you should add. I won't go into details of working with MapStruct here. You can read more about creating DTOs with Mapstruct in my previous guide. The code for DTOs and Mapper: @Builder public record EmployeeDto(int id, String firstName, String lastName) { } @Builder public record NewEmployeeDto(String firstName, String lastName) { } @Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = "spring") public class EmployeeMapper { public EmployeeDto mapToEmployeeDto(Employee employee) { return EmployeeDto.builder() .id(employee.getId()) .lastName(employee.getLastName()) .firstName(employee.getFirstName()).build(); } public Employee mapToEmployee(EmployeeDto employeeDto) { return Employee.builder() .id(employeeDto.id()) .firstName(employeeDto.firstName()) .lastName(employeeDto.lastName()) .build(); } public Employee mapToEmployee(NewEmployeeDto employeeDto) { return Employee.builder() .firstName(employeeDto.firstName()) .lastName(employeeDto.lastName()) .build(); } } Next, let's create EmployeeController, EmployeeRepository interface, and EmployeeService. Populate the classes as shown below. I have also added a custom NotFoundException with relevant ErrorHandler. public interface EmployeeRepository extends ListCrudRepository { } @Service public class EmployeeService { private final EmployeeRepository employeeRepository; private final EmployeeMapper mapper; public EmployeeService(EmployeeRepository employeeRepository, EmployeeMapper employeeMapper) { this.employeeRepository = employeeRepository; this.mapper = employeeMapper; } public List findAll() { return employeeRepository.findAll() .stream() .map(mapper::mapToEmployeeDto) .toList(); } public EmployeeDto findById(int id) { Optional employee = employeeRepository.findById(id); if (employee.isEmpty()) { throw new NotFoundException("Employee not found with id: " + id); } return mapper.mapToEmployeeDto(employee.get()); } public EmployeeDto save(NewEmployeeDto employeeDto) { Employee employee = mapper.mapToEmployee(employeeDto); return mapper.mapToEmployeeDto(employeeRepository.save(employee)); } public EmployeeDto update(EmployeeDto employeeDto) { Employee employee = mapper.mapToEmployee(employeeDto); return mapper.mapToEmployeeDto(employeeRepository.save(employee)); } public void deleteById(int id) { if (!employeeRepository.existsById(id)) { throw new NotFoundException("Employee not found with id: " + id); } employeeRepository.deleteById(id); } } @RestController public class EmployeeController { private final EmployeeService employeeService; public EmployeeController(EmployeeService service) { this.employeeService = service; } @GetMapping("/employees") public List findAllEmployees() { return employeeService.findAll(); } @GetMapping("/employees/{employeeId}") public EmployeeDto getEmployee(@Parameter( description = "ID of employee to be retrieved", required = true) @PathVariable int employeeId) { return employeeService.findById(employeeId); } @PostMapping("/employees") public EmployeeDto addEmployee(@RequestBody NewEmployeeDto employee) { return employeeService.save(employee); } @PutMapping("/employees") public EmployeeDto updateEmployee(@RequestBody EmployeeDto employee) { return employeeService.update(employee); } @DeleteMapping("/employees/{employeeId}") public String deleteEmployee(@PathVariable int employeeId) { employeeService.deleteById(employeeId); return "Deleted employee with id: " + employeeId; } } Now, let’s provide a database schema. Create a schema.sql file in the resources directory with the following content: create table if not exists employee ( id serial primary key, first_name varchar(255) not null, last_name varchar(255) not null ); Next, let’s populate our database instance with some data (that’s optional, we don’t need to work with DB data when developing APIs, I just don’t like to see the ugly error page upon starting the app in the browser). Create the data.sql file in resources with the following content: delete from employee; insert into employee (first_name, last_name) values ('John', 'Doe'); insert into employee (first_name, last_name) values ('Jane', 'Smith'); The last thing to do is to add several properties to the application.properties file because we are performing a script-based initialization: database=h2 spring.sql.init.schema-locations=classpath*:schema.sql spring.sql.init.data-locations=classpath*:data.sql spring.sql.init.mode=always Finally, let’s verify that the app is functioning as desired. Run the application. You should see the following result at http://localhost:8080/employees: [ { "id": 1, "firstName": "John", "lastName": "Doe" }, { "id": 2, "firstName": "Jane", "lastName": "Smith" } ] That’s it! Our minimalistic CRUD application is ready for experiments. Add springdoc-openapi dependency To work with Swagger, we need the springdoc-api library that helps to generate OpenAPI-compliant API documentation for Spring Boot projects. The library supports Swagger UI and other useful features such as OAuth2 and GraalVM Native Image. Add the following dependency for springdoc-api to your pom.xml file: org.springdoc springdoc-openapi-starter-webmvc-ui 2.2.0 That’s all, no additional configuration is required! Generate API documentation The OpenAPI documentation is generated when we build our project. So let’s verify that everything is working correctly. Run your application and go to the default page where the API documentation is located: http://localhost:8080/v3/api-docs. You should see the data on your endpoints in JSON format. You can also access the .yaml file at http://localhost:8080/v3/api-docs.yaml. It is possible to change the default path in the application.properties file. For example: springdoc.api-docs.path=/api-docs Now the documentation is available at http://localhost:8080/api-docs. Integrate Swagger UI The beauty about springdoc-openapi library dependency is that it already includes Swagger UI, so we don’t have to configure the tool separately! You can access Swagger UI at http://localhost:8080/swagger-ui/index.html, where you will see a beautiful user interface to interact with your endpoints (or similar to the one on the screenshot if you are using your project): Swagger UI Configure Swagger 3 in Spring Boot with annotations Right now, our API documentation is not very informative. We can extend it with the help of annotations added to the application code. Below is the summary of the most common ones. Add Swagger API description First of all, let’s include some essential data about the API, such as name, description, and author contacts. For that purpose, create an OpenAPIConfiguration class and fill in the following code: @Configuration public class OpenAPIConfiguration { @Bean public OpenAPI defineOpenApi() { Server server = new Server(); server.setUrl("http://localhost:8080"); server.setDescription("Development"); Contact myContact = new Contact(); myContact.setName("Jane Doe"); myContact.setEmail("your.email@gmail.com"); Info information = new Info() .title("Employee Management System API") .version("1.0") .description("This API exposes endpoints to manage employees.") .contact(myContact); return new OpenAPI().info(information).servers(List.of(server)); } } You can also fill in information about applicable License and some other data, but code above is enough for demonstration. Run the app and verify that the main API page includes provided information: API description Bean validation The springdoc-openapi library supports JSR 303: Bean Validation (@NotNull, @Min, @Max, and @Size), so when we add these annotations to our code, the additional schema documentation will be automatically generated. Let’s specify them in Employee class: public class Employee { @Id private int id; @NotNull @Size(min = 1, max = 20) private String firstName; @NotNull @Size(min = 1, max = 50) private String lastName; When you recompile your app, you will see that the Schemas section contains the specified info: API Schema @Tag annotation The @Tag annotation can be applied at class or method level and is used to group the APIs in a meaningful way. For instance, let’s add this annotation to our GET methods: @Tag(name = "get", description = "GET methods of Employee APIs") @GetMapping("/employees") public List findAllEmployees() { return employeeService.findAll(); } @Tag(name = "get", description = "Retrieve one employee") @GetMapping("/employees/{employeeId}") public EmployeeDto getEmployee( @PathVariable int employeeId) { return employeeService.findById(employeeId); } You will see that APIs are now grouped differently: API grouping @Operation annotation The @Operation annotation enables the developers to provide additional information about a method, such as summary and description. Let’s update our updateEmployee() method: @Operation(summary = "Update an employee", description = "Update an existing employee. The response is updated Employee object with id, first name, and last name.") @PutMapping("/employees") public EmployeeDto updateEmployee(@RequestBody EmployeeDto employee) { return employeeService.update(employee); } The API description in Swagger UI is now a little more informative: Endpoint description @ApiResponses annotation The @ApiResponses annotation helps to add information about responses available for the given method. Each response is specified with @ApiResponse, for instance @ApiResponses({ @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Employee.class))}), @ApiResponse(responseCode = "404", description = "Employee not found", content = @Content)}) @DeleteMapping("/employees/{employeeId}") public String deleteEmployee(@PathVariable int employeeId) { employeeService.deleteById(employeeId); return "Deleted employee with id: " + employeeId; } After you recompile the Controller class, the data on responses will be automatically generated: API responses @Parameter annotation The @Parameter annotation can be used on a method parameter to define parameters for the operation. For example, public EmployeeDto getEmployee(@Parameter( description = "ID of employee to be retrieved", required = true) @PathVariable int employeeId) { return employeeService.findById(employeeId); } Here, the description element provides additional data on parameter purpose, and required is set to true signifying that this parameter is mandatory. And here’s how it looks in Swagger UI: Endpoint parameters Conclusion As you can see, writing APIs with Swagger is extremely convenient. The solution enables declarative and consistent API documentation without laborious manual effort. As a result, it accelerates the development process and minimizes the risk of errors, so you should definitely integrate it into your workflow! And if you are looking to optimize resourse consumption of your containerized Spring Boot apps, try out Alpaquita Containers tailor-made for Spring Boot and helping to achieve up to 30 % RAM savings! - [Spring Boot monitoring in Kubernetes with Prometheus and Grafana](https://bell-sw.com/blog/spring-boot-monitoring-in-kubernetes-with-prometheus-and-grafana/): The application is successfully deployed to Kubernetes, and the instances are running normally — the job is done, right? Yes and no. A lot is happening in your clusters, and monitoring their health is essential to solve potential issues as soon as they arise so as not to disturb user experience and tailor the KPIs to the business needs. In addition, we need to understand how an application behaves and how much memory it actually needs to select the appropriate instance size. By the way, to optimize instance size and reduce the startup and warmup times of your services from minutes to milliseconds, which is vital for high availability, you can use Java with CRaC. You should definitely give it a try! Coming back to monitoring. Fortunately, Spring Boot provides the Actuator tool capable of exporting numerous out-of-the-box and custom application metrics. The metrics can be collected with Prometheus and visualized with Grafana, two outstanding open-source monitoring solutions. If you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot! Table of Contents Why Prometheus and Grafana Prerequisites Enable Spring Boot Actuator Make Actuator metrics visible to Prometheus Add custom metrics Set up Prometheus and Grafana on Kubernetes Install Prometheus Install Grafana Couple Prometheus with Grafana Deploy the application to Kubernetes Visualize the metrics with Grafana Conclusion Why Prometheus and Grafana Prometheus is an open-source solution for time series (i.e., with a timestamp of each recording) data collection and monitoring. Being a graduated Cloud Native Computing Foundation (CNCF) project with 50K+ stars on GitHub, it is a leading monitoring system in the niche. A distinctive feature of Prometheus is standalone server nodes that don’t depend on network storage, providing enhanced reliability and access to collected data even under failure conditions. While Prometheus offers a robust time series metrics database, Grafana helps to visualize the collected data most conveniently with the help of a beautiful user interface. The Grafana UI includes a dashboard with colorful charts, graphs, and alerts, facilitating the analysis of complex data sets. Prerequisites JDK 17 (I will use Liberica JDK, recommended by Spring) Docker minikube (refer to our installation guide if you don’t have it yet) Maven Ready application (I will use the one from the previous article on Spring Boot deployment to Kubernetes; you can work with yours if you wish) The code for the project below is available on GitHub. Enable Spring Boot Actuator First thing first, let’s enable Spring Actuator to expose application metrics for Prometheus to gather. Add the following dependency to the pom.xml file: org.springframework.boot spring-boot-starter-actuator To expose the metrics, we need to explicitly specify them in our application.properties file: management.endpoints.web.exposure.include=health,metrics That’s it! Run the application, and at http://localhost:8080/actuator/health you should see { "status": "UP" } Now, go to http://localhost:8080/actuator/metrics, and you will see a long list of available metrics (some of them are shown below): { "names": [ "application.ready.time", "application.started.time", "disk.free", "disk.total", "executor.active", "executor.completed", "executor.pool.core", "executor.pool.max", "executor.pool.size", "executor.queue.remaining", "executor.queued", "http.server.requests", "http.server.requests.active", "jvm.buffer.count", "jvm.buffer.memory.used", "jvm.buffer.total.capacity", "jvm.classes.loaded", "jvm.classes.unloaded", "jvm.compilation.time", "jvm.gc.live.data.size", "jvm.gc.max.data.size", "jvm.gc.memory.allocated", "jvm.gc.memory.promoted", "jvm.gc.overhead", "jvm.info", "jvm.memory.committed", "jvm.memory.max", "jvm.memory.usage.after.gc", "jvm.memory.used", … Each metric can be studied separately by adding the value to the URL. For instance, let’s look more closely at jvm.memory.max at http://localhost:8080/actuator/metrics/jvm.memory.max: { "name":"jvm.memory.max", "description":"The maximum amount of memory in bytes that can be used for memory management", "baseUnit":"bytes", "measurements": [ { "statistic":"VALUE", "value":5620367357 } ], "availableTags": [ { "tag":"area", "values": [ "heap", "nonheap" ] }, { "tag":"id", "values": [ "CodeHeap 'profiled nmethods'", "G1 Old Gen", "CodeHeap 'non-profiled nmethods'", "G1 Survivor Space", "Compressed Class Space", "Metaspace", "G1 Eden Space", "CodeHeap 'non-nmethods'" ] } ] } Make Actuator metrics visible to Prometheus The next step is to make application metrics visible to Prometheus. For that purpose, add the Micrometer dependency to pom.xml: io.micrometer micrometer-registry-prometheus Update the application.properties file: management.endpoints.web.exposure.include=health,metrics,prometheus The new endpoint will be available at http://localhost:8080/actuator/prometheus. Below is only a small part of what you will see there when you run your application: # HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time # TYPE system_load_average_1m gauge system_load_average_1m 1.41015625 # HELP system_cpu_usage The "recent cpu usage" of the system the application is running in # TYPE system_cpu_usage gauge system_cpu_usage 0.11948988078735792 # HELP jvm_info JVM version info # TYPE jvm_info gauge jvm_info{runtime="OpenJDK Runtime Environment",vendor="BellSoft",version="17.0.7+7-LTS",} 1.0 # HELP process_files_open_files The open file descriptor count # TYPE process_files_open_files gauge process_files_open_files 62.0 # HELP jvm_gc_pause_seconds Time spent in GC pause # TYPE jvm_gc_pause_seconds summary jvm_gc_pause_seconds_count{action="end of minor GC",cause="G1 Evacuation Pause",gc="G1 Young Generation",} 1.0 jvm_gc_pause_seconds_sum{action="end of minor GC",cause="G1 Evacuation Pause",gc="G1 Young Generation",} 0.003 Add custom metrics You can use custom metrics to monitor parameters important for your workloads. These metrics are automatically picked up by Prometheus. For instance, you can use a Counter to measure the number of requests made to the application or a Timer to measure latency. As a demonstration, let’s count the sum of all numbers up to 1,000 with an interval of 10 ms: @RestController public class TimerController { public TimerController(MeterRegistry registry){ Timer timer = registry.timer("Time for operation"); timer.record(() -> { int sum = 0; for(int i=0; i<= 1000; i++ ){ sum += i; try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }); } Run the application. At actuator/prometheus you will find our custom metrics with information that it took the app 12.1 seconds to complete the task. # HELP Time_for_operation_seconds # TYPE Time_for_operation_seconds summary Time_for_operation_seconds_count 1.0 Time_for_operation_seconds_sum 12.209412708 Set up Prometheus and Grafana on Kubernetes Up until now, we have monitored our application on bare metal. However, there are more things to do before we can monitor our containerized workloads on Kubernetes. First, we must deploy Prometheus and Grafana to our Kubernetes cluster. We will use Helm charts to install the tools on Kubernetes. Helm is another open-source graduated CNCF project aimed at facilitating the management of Kubernetes applications. It provides a solution for defining, deploying, and upgrading any Kubernetes application with the help of Kubernetes packages called charts. There are numerous ready charts available, including those for Prometheus and Grafana. Install Prometheus First of all, you need to install Helm CLI if you don’t have it yet. There are several ways of doing that. You can download a binary version for your operating system, unpack it, and move to the required location; use the installer script that will install the latest version of Helm to your machine: $ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 $ chmod 700 get_helm.sh $ ./get_helm.sh take advantage of package managers (Homebrew, Chocolatey, apt, dnf/yum, etc.). For instance, to install Helm with Homebrew, run $ brew install helm After installing Helm CLI, install the Bitnami Helm repository where Prometheus charts reside: $ helm repo add bitnami https://charts.bitnami.com/bitnami $ helm repo update The next step is to pull the Prometheus Helm chart. Developers can take advantage of various configuration options to tailor Prometheus to their deployment environment. For our simple demo app though, the default settings will suffice. Run $ helm install prometheus bitnami/kube-prometheus NAME: prometheus LAST DEPLOYED: Wed Nov 1 11:54:58 2023 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: CHART NAME: kube-prometheus CHART VERSION: 8.22.0 APP VERSION: 0.68.0 ** Please be patient while the chart is being deployed ** Watch the Prometheus Operator Deployment status using the command: kubectl get deploy -w --namespace default -l app.kubernetes.io/name=kube-prometheus-operator,app.kubernetes.io/instance=prometheus Watch the Prometheus StatefulSet status using the command: kubectl get sts -w --namespace default -l app.kubernetes.io/name=kube-prometheus-prometheus,app.kubernetes.io/instance=prometheus Prometheus can be accessed via port "9090" on the following DNS name from within your cluster: prometheus-kube-prometheus-prometheus.default.svc.cluster.local To access Prometheus from outside the cluster execute the following commands: echo "Prometheus URL: http://127.0.0.1:9090/" kubectl port-forward --namespace default svc/prometheus-kube-prometheus-prometheus 9090:9090 Watch the Alertmanager StatefulSet status using the command: kubectl get sts -w --namespace default -l app.kubernetes.io/name=kube-prometheus-alertmanager,app.kubernetes.io/instance=prometheus Alertmanager can be accessed via port "9093" on the following DNS name from within your cluster: prometheus-kube-prometheus-alertmanager.default.svc.cluster.local To access Alertmanager from outside the cluster execute the following commands: echo "Alertmanager URL: http://127.0.0.1:9093/" kubectl port-forward --namespace default svc/prometheus-kube-prometheus-alertmanager 9093:9093 The default Prometheus pod is accessible from within the cluster only, which is a better practice than exposing Prometheus metrics to the outside world. As you can see above, Helm advises you on accessing Prometheus, including port-forward — let’s make use of it. Open a new Terminal window and run $ kubectl port-forward --namespace default svc/prometheus-kube-prometheus-prometheus 9090:9090 Voilà! Prometheus is now accessible via http://localhost:9090. Prometheus GUI Install Grafana We are going to install Grafana in a similar fashion, with the help of a dedicated Helm chart. Again, you can use default settings for now and make a deep dive into available configurations for your enterprise project. To install Grafana, run $ helm install grafana bitnami/grafana NAME: grafana LAST DEPLOYED: Wed Nov 1 12:25:23 2023 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: CHART NAME: grafana CHART VERSION: 9.5.0 APP VERSION: 10.2.0 ** Please be patient while the chart is being deployed ** 1. Get the application URL by running these commands: echo "Browse to http://127.0.0.1:8080" kubectl port-forward svc/grafana 8080:3000 & 2. Get the admin credentials: echo "User: admin" echo "Password: $(kubectl get secret grafana-admin --namespace default -o jsonpath="{.data.GF_SECURITY_ADMIN_PASSWORD}" | base64 -d)" Next, retrieve the password generated by the Helm chart to access Grafana (the command is conveniently provided by Helm as you can see above): $ echo "Password: $(kubectl get secret grafana-admin --namespace default -o jsonpath="{.data.GF_SECURITY_ADMIN_PASSWORD}" | base64 -d)" Now we can access Grafana with the port-forward command. In a new Terminal window, execute: $ kubectl port-forward svc/grafana 8080:3000 Visit the http://localhost:8080. Enter the username (admin by default) and the password that you retrieved previously. Upon successful login, you will see the main Grafana page. Grafana GUI Couple Prometheus with Grafana The last thing we need to do is to make Prometheus metrics visible in the Grafana dashboard. Click on “Add your first data source” and choose Prometheus on the list. Enter the URL where Prometheus is running (http://prometheus-kube-prometheus-prometheus.default.svc.cluster.local:9090). Click “Save & Test.” You can now import various dashboards and panels for data visualization. Grafana offers numerous ready dashboards: all you need to do is to import the ID of a selected panel to your Grafana installation. All set, it’s time to poke into our containerized app and see how it is doing! Deploy the application to Kubernetes I’m assuming that you are familiar with the process of deploying an application to Kubernetes, but if you haven’t familiarized yourself with the procedure, please refer to my previous guide with step-by-step instructions. Containerize your application and push it to a container registry — this is where Kubernetes pulls the images. As I said in the beginning, I’m using an image from the previous guide, built with a lightweight Alpaquita Container tailor-made for Spring Boot. What we need to do now is create a deployment.yaml file with the following content (substitute with your Docker ID if you published the image to the Docker Hub registry): apiVersion: apps/v1 kind: Deployment metadata: name: spring-boot-app labels: app: spring-boot-app spec: replicas: 1 selector: matchLabels: app: spring-boot-app template: metadata: labels: app: spring-boot-app spec: containers: - name: spring-boot-app image: /spring-boot-app imagePullPolicy: Always ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: spring-boot-app-service labels: app: spring-boot-app spec: selector: app: spring-boot-app ports: - protocol: TCP name: http-traffic port: 8080 targetPort: 8080 --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: spring-boot-app-service-monitor spec: selector: matchLabels: app: spring-boot-app endpoints: - port: http-traffic path: "/actuator/prometheus" Apart from the usual Deployment and Service parts, we specify a ServiceMonitor that defines endpoints to scrape metrics. Make sure that minikube is running. Then, go to the application directory and deploy the .yaml file: $ kubectl apply -f deployment.yaml You can access your pods by running $ kubectl get all NAME READY STATUS RESTARTS AGE pod/alertmanager-prometheus-kube-prometheus-alertmanager-0 2/2 Running 0 19m pod/grafana-84776fc997-srqvk 1/1 Running 0 17m pod/prometheus-kube-prometheus-blackbox-exporter-77b4db9fd8-wbflx 1/1 Running 0 20m pod/prometheus-kube-prometheus-operator-7ff7699948-mfkwb 1/1 Running 0 20m pod/prometheus-kube-state-metrics-5dcf55d96d-92nj6 1/1 Running 0 20m pod/prometheus-node-exporter-m97wr 1/1 Running 0 20m pod/prometheus-prometheus-kube-prometheus-prometheus-0 2/2 Running 0 19m pod/spring-boot-app-dbf645dbf-hrb9c 1/1 Running 0 13m Prometheus should pick up the new endpoint, which can be seen in the “Status -> Targets” section at http://localhost:9090. Application endpoint picked up by Prometheus In the Graph section, you can check application metrics, including our custom one. Displaying custom metrics Visualize the metrics with Grafana The final touch — let’s head to Grafana and visualize our metrics there. The most convenient method of visualizing application metrics is to import a ready dashboard. For instance, let’s import a dashboard for Kubernetes Cluster Monitoring. You will see the memory, CPU, and filesystem usage stats for your cluster, as well as network and pods I/O data. Visualizing metrics with Grafana Conclusion As you can see, setting up a monitoring solution for your Spring Boot application is not complicated. As a result, you can see everything happening in your Kubernetes cluster and thus react quickly to undesirable changes or tune the performance based on real-life data. Want to know more valuable tips on running Spring Boot in the cloud? Subscribe to our newsletter to stay up to date with the newest guides! - [New Spring Boot 3.2 version goes GA](https://bell-sw.com/blog/new-spring-boot-3-2-version-goes-ga/): Today is an exciting day for all Spring Boot developers as Spring Boot 3.2 packed with new and enhanced features was released! The highlights of this release include but are not limited to JVM Coordinated Restore at Checkpoint (CRaC), support for Virtual threads, support for Java 21 LTS, enhanced AppCDS support. Let’s dive into the new functionality in more detail and see how it will empower your Spring Boot applications. Table of Contents CRaC support Enhanced AppCDS preparing the grounds for Project Leyden Java 21 LTS Virtual threads (Project Loom) Boost your Java project with new Spring Boot and Alpaquita Containers! CRaC support Coordinated Restore at Checkpoint (CRaC) is a JVM API that enables the developers to create an image of a running Java application,and then restore the application from the file at the exact state when the checkpoint was made. This way, the startup and warmup times reduce from seconds to milliseconds without any performance losses! CRaC is a promising functionality in terms of lowering cloud costs by minimizing the time you pay for resource usage and optimizing the instance size. With this release, Spring Boot integrates with CRaC seamlessly, enabling you to use the feature out-of-the-box. Prerequisites to utilizing CRaC with Spring: JVM with CRaC support, The org.crac:crac library version 1.4.0 or higher, The java command line parameters (such as -XX:CRaCCheckpointTo=PATH or -XX:CRaCRestoreFrom=PATH) specification. BellSoft provides a special build of Liberica JDK, the default runtime for Spring, with CRaC so that Spring Boot users can enjoy an all-round experience with Coordinated Restore at Checkpoint when developing and deploying their projects. Find out how to use CRaC with the new Spring Boot and Liberica JDK here. Enhanced AppCDS preparing the grounds for Project Leyden Application Class Data Sharing (AppCDS) is a JVM feature that enables developers to cut down the startup time and memory footprint of Java applications. With AppCDS, the JVM can create pre-processed archives of classes loaded at startup, and then share them between JVM instances on the same host. As a result, there’s no need to parse and verify the classes at each startup and duplicate them in memory. The Spring Framework 6.1 and consequently, Spring Boot 3.2 offer an enhanced support for AppCDS, enabling the developers to use this functionality in a more convenient way. Java 21 LTS JDK 21 is the latest Java LTS release, and as any LTS version intended for long-term enterprise use, it is full of major enhancements and novelties. 15 JEPs in total are aimed at improving the performance (virtual threads, generational ZGC, etc.), convenience (e.g., unnamed pattern and variables), and security (the key encapsulation mechanism API) of the Java platform. If you wish to learn more about all JDK 21 features, refer to our previous article. While Spring Boot 3.2 maintains JDK 17 as a baseline, it is fully compatible with Java 21. Virtual threads (Project Loom) Virtual threads in the novel JDK feature developed under the auspices of Project Loom. It was first introduced in JDK 19 and finalized in JDK 21. Virtual threads bring the development of concurrent high-throughput Java applications to the next level because they enable developers to create thousands of lightweight threads, which can be managed, monitored, and debugged just like the usual threads. Spring Boot 3.2 provides full support for virtual threads together with many additional configuration options aimed at making the process of working with this feature more convenient and reliable. To use virtual threads with the new Spring Boot, developers need to use JDK 21 and set the spring.threads.virtual.enabled property to true. Boost your Java project with new Spring Boot and Alpaquita Containers! More on Spring Boot 3.2 features and enhancements you can find in the Release Notes. The more efficient development couples nicely with efficient deployment when using Alpaquita Containers tailor-made for Spring Boot apps. Based on a lightweight Alpaquita Linux and Liberica JDK, these containers enable the developers to save up to 30 % of RAM and help to secure the project thanks to additional security hardening and regular updates both for JDK and Linux. Try the new Spring Boot with Alpaquita Containers and tell us what you think! - [How to Migrate from Oracle JDK to OpenJDK](https://bell-sw.com/blog/how-to-migrate-from-oracle-jdk-to-openjdk/): If you have been a long-term Oracle customer, migration from Oracle Java to an open-source OpenJDK distribution (for some, as good as diving into the unknown) seems to be a tall order. However, the key is to break the task down into manageable steps. BellSoft helped numerous enterprises migrate from Oracle JDK to OpenJDK, gaining extensive experience in the process. Our experience allowed us to develop a strategy for smooth and painless migration to OpenJDK. Don’t worry; with BellSoft, migration from Oracle is not as painful as it may appear. Below is a surefire roadmap for efficient and comfortable migration. Table of Contents Stage 1. Make an inventory of all enterprise Java runtimes Step 1. Create inventory of all Java runtimes in your fleet Step 2. Identify which JDKs you actually need Step 3. Determine Java versions you use now or plan to integrate later Step 4. Features from older Oracle Java versions Step 5. Specify the platforms and environments in use now or to be introduced later Stage 2. Change all Java runtimes to the chosen OpenJDK distribution Stage 3. Verify that everything works correctly Need help with the migration? BellSoft experts are ready to support you! Stage 1. Make an inventory of all enterprise Java runtimes Step 1. Create inventory of all Java runtimes in your fleet The initial stage of the migration process involves creating an inventory of all Java runtimes installed in your corporate environment. To do this, you can use IT Asset Management (ITAM) tools, designed to gather information about IT assets (in our case, JDK) across the network. For Windows-based systems, you can use Liberica Administration Center (LAC). It conducts a thorough scan of all machines, identifying: JDKs from different vendors installed on the standard path; JDKs tied to keys in Windows Registry; Running processes that use Java (e.g., idea.exe) along with the corresponding JDK installations. LAC efficiently gathers crucial information about Java runtimes, including versions, end-of-life notices, vulnerabilities, and potential licensing risks. This information is then conveniently aggregated into a comprehensive report. For other systems (or if you don’t use any ITAM solution), manual inventory becomes inevitable. It is crucial to leave no stone unturned, as any neglected JDK left hanging on your machine may pose potential licensing issues. Step 2. Identify which JDKs you actually need When you list all JDKs installed in your corporate fleet, you may notice that some runtimes are redundant because they are no longer used with corporate workloads. In such cases, you can safely delete them, reducing JDK licensing costs. However, if your company relies on third-party Java applications that come with a runtime, it’s crucial to consider the support terms associated with this application because they may cover the JDK. In addition, the vendor may only provide support if you use the bundled JDK or the one from the vendor’s list. Nevertheless, the vendor may agree to support the application even if you utilize another JDK, provided that it fully complies with the Java SE standards. To ensure compliance, specific tests are conducted, known as Technology Compatibility Kit or TCK. Only Java runtimes that successfully passed the TCK verification can guarantee seamless migration. In any case, consulting the application vendor for detailed information is strongly recommended. Step 3. Determine Java versions you use now or plan to integrate later Corporate Java fleet is often diverse. Java’s excellent backward compatibility enables the developers to introduce new services and technologies running on newer Java versions without upgrading the existing stack. This means that some companies may have older services running on Java 6 & 7 (or even earlier versions), while recently added services utilize JDK 17 or JDK 21. At the same time, you may soon want to take advantage of the latest and greatest JDK versions, each incorporating dozens of innovative features (such as virtual threads in JDK 21). Either way, finding a vendor that supports a wide range of Java versions is crucial. Some OpenJDK vendors support only LTS versions, but only two, including BellSoft, offer extended support for Java 6 & 7 with regular security patches and updates. Step 4. Features from older Oracle Java versions Starting with Java 11, Oracle JDK and OpenJDK features are compatible. However, migrating from older Oracle Java versions requires special consideration as they contain technologies that weren’t released into open source: JavaFX: Developed as a separate OpenJDK project, OpenJFX, but it’s not included in the OpenJDK binaries by default. Certain OpenJDK vendors, such as BellSoft, offer JDK builds bundled with OpenJFX. Java Web Start: Deprecated in Oracle JDK 9 and removed from Oracle JDK 11. However, this technology remains closed-source. For those dependent on it, seek a vendor supporting the open-source reimplementation of Web Start like OpenWebStart. Applets: Still supported on Windows as part of Oracle Java 8 and earlier, but modern browsers don’t support the technology for security reasons. No OpenJDK vendor offers support for applets. Lucida fonts family: Used in Oracle JDK 8 and earlier. These fonts aren’t included in OpenJDK. Switch to other fonts supported by the JDK (use the GraphicsEnvironment class to find out which fonts are available) or acquire them separately or as part of other software products. Java Control Panel: Not part of the OpenJDK, but the keytool utility can be used instead. Windows Registry Keys: add registry keys for an application to locate a Java runtime. Some vendors may provide different functionality. Regarding registry keys, Liberica JDK offers similar settings. For instance, let’s look at installing Liberica JDK 11 on Windows using a .msi file and how registry keys are added. Subfeatures can be selectively chosen. Set the JAVA_HOME accordingly and set the association with the Jar. After that, you can double-click any .jar file, and Liberica JDK will launch it. The registry keys setup varies depending on 32- or 64-bit architecture, as well as Java version Step 5. Specify the platforms and environments in use now or to be introduced later In addition to determining the required Java versions, it’s crucial to identify the platforms your company currently uses or plans to integrate in the future. The understanding of your tech stack is vital for selecting a Java runtime compatible with the necessary architectures and operating systems. The platform inventory should include the cloud environment if you run cloud-native applications or consider moving your server workloads to the cloud. While some cloud vendors provide a JDK distribution as part of their offering, it may not always meet your specific requirements for versions, features, performance (e.g., memory consumption), or SLA. Unifying the JDKs across development, CI (building and testing), and production environments is optimal. This approach reduces complexity, accelerates time-to-market by ensuring consistent application performance across all environments without the need for reconfiguration, and lowers the total cost of ownership (TCO). Even with a unified Java runtime across the enterprise, don’t neglect the containers, a cornerstone of cloud deployments. A perfect container balances a small size with enterprise-grade security. To minimize a container size, developers may choose lightweight Alpine Linux, suitable for optimizing memory footprint but lacking security features for enterprise development. A perfect solution is to select a vendor providing a Java runtime and optimized containers with updates and support for both Linux and Java. BellSoft offers containers tailor-made for Java development. They are based on Liberica JDK Lite, a Liberica JDK flavor with multiple backports from recent versions and better compression for a reduced container footprint, and Alpaquita Linux, inspired by Alpine but boasting additional security strengthening: kernel hardening, regular updates, LTS releases, enhanced flexibility: two libc implementations (optimized musl and glibc, four memory allocators. Alpaquita Containers, based on Liberica JRE Lite and musl-based Alpaquita, take up tens of megabytes compared to many others that can be hundreds of megabytes. As a result, they enable significant cloud cost savings. In addition, regular updates for JDK and Linux keep these containers secure and performant. Stage 2. Change all Java runtimes to the chosen OpenJDK distribution Once you’ve gathered information about all corporate JDKs and decided which ones to migrate, the migration process can commence. Download the bundle from the selected vendor and replace Oracle JDK with the new distribution. Note that if you’re using Liberica Administration Center for Windows-based systems, you can conveniently update all Java runtimes from a single window. Otherwise, various installation paths are available, allowing you to choose the most convenient one. Below, we outline possible installation methods of Liberica JDK (note that other vendors may offer different installation formats). Delivery channel Description Binaries on the website Download binaries for Windows, macOS, and Linux in various formats: .msi, .dmg, .pkg, .zip, .tar.gz, .deb, .rpm, .apk Container registries Pull Liberica JDK images from Docker Hub, GHCR, and MCR REST Discovery API Find and download the necessary Liberica JDK versions via REST API Package managers Liberica JDK builds are available in SDKMAN, Brew, SCOOP, winget Linux repositories Pull Liberica JDK from YUM, APT, Zypper, APK Buildspacks Use buildpacks with Liberica JDK or Alpaquita Linux + Liberica JDK Lite to automate containerization Cloud instances Ready Liberica JDK instances are available in AWS and Azure. Once you’ve chosen the installation path, proceed to downloading and installing the necessary versions. At this stage, upgrading the Java version (e.g., from JDK 8 to 11) is unnecessary. While it’s recommended to install the latest minor update for security considerations, you can opt out for a version equivalent to the one currently installed to avoid the risk of regressions. Updates within one version may differ significantly due to the introduced fixes and changes. When performing the update, categorize your services into less and more critical ones. Less critical services can be updated in one go, whereas critical ones require additional attention in case something needs to be fixed. After the update, change the JAVA_HOME environment variable and other relevant configurations to point to a new JDK. Stage 3. Verify that everything works correctly After migrating all your applications, run tests to ensure that they function without failures and regressions. If the selected JDK is TCK-verified and versions are compatible, you’re unlikely to encounter any issues. Note, however, that the older the Java version is, the more “surprises” it may reveal upon migration. In addition, if you’ve opted out for a newer version and run into errors, the cause probably lies in the changes introduced between versions. Either way, if you subscribed to commercial support, your vendor will help you root cause and promptly resolve any issues that may arise. Need help with the migration? BellSoft experts are ready to support you! While the key steps of the migration plan are universal, every enterprise project is unique. BellSoft engineers are here to guide you through the entire process, ensuring a smooth and efficient migration. We’ll help address any potential issues at any stage. Contact our migration experts today and embark on a path to highly performant, secure, and cost-efficient Java development! Book a consultation - [An overview of JDK 22 features](https://bell-sw.com/blog/an-overview-of-jdk-22-features/): JDK 22 entered the Rampdown Phase Two on January 18, an excellent occasion to peek at the JEPs awaiting us in this feature release! JDK 22 is the first release after LTS JDK 21, but the community is buzzing with activity: the feature version includes 12 JEPs with new and enhanced functionality. Table of Contents JEPs targeted to JDK 22 JEP 423: Region Pinning for G1 JEP 447: Statements before super(...) (Preview) JEP 454: Foreign Function & Memory API JEP 456: Unnamed Variables & Patterns JEP 457: Class-File API (Preview) JEP 458: Launch Multi-File Source-Code Programs JEP 459: String Templates (Second Preview) JEP 460: Vector API (Seventh Incubator) JEP 461: Stream Gatherers (Preview) JEP 462: Structured Concurrency (Second Preview) JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) JEP 464: Scoped Values (Second Preview) Get the performance of newer JDK versions without migration! JEPs targeted to JDK 22 JEP 423: Region Pinning for G1 JEP 423 aims to reduce latency during garbage collection by not disabling G1 GC in the presence of Java Native Interface (JNI) critical regions. JNI uses pointers for relevant Java objects to work with unmanaged programming languages. The memory regions where these objects reside are considered critical regions, and these objects, when in use, should not be moved during garbage collection. Consequently, G1 GC disables garbage collection when a thread is in a critical region, meaning it must wait until no threads are in critical regions when garbage collection is triggered. It may lead to significant latency (up to several minutes), OutOfMemory errors, and VM shutdown. As G1 GC could already pin certain regions to their memory locations during major collections and perform collection without touching them, this JEP extends this functionality and enables G1 GC to pin regions during minor and major collections. As a result: pinned regions in young generation will be promoted to old generation, and pinned regions in old generation will not be evacuated, and consequently, garbage collection can be performed normally even in the presence of critical regions. JEP 447: Statements before super(...) (Preview) Thanks to Java’s natural polymorphism, classes can conveniently extend other classes, inheriting their functionality and adding their own. For this to work, Java constructors must be initialized from the top down, i.e., the constructor in the superclass must first initialize the fields declared in this class, and only after that can the constructor in the subclass run. To guarantee this behavior, Java requires in the constructor body, to place explicit invocation of another constructor as the first statement, like this: public class PositiveBigInteger extends BigInteger { public PositiveBigInteger(long value) { super(value); if (value <= 0) throw new IllegalArgumentException("non-positive value"); } } However, this sometimes leads to excessive verbosity and code complexity. For instance, in a snippet above, we need to add an additional method if we want to validate the constructor fields before invoking the constructor of a superclass: public class PositiveBigInteger extends BigInteger { public PositiveBigInteger(long value) { super(verifyPositive(value)); } private static long verifyPositive(long value) { if (value <= 0) throw new IllegalArgumentException("non-positive value"); return value; } } JEP 447 enables the developers to place statements that do not reference the instance being created before an explicit constructor invocation. So the snippet above can be transformed into: public class PositiveBigInteger extends BigInteger { public PositiveBigInteger(long value) { if (value <= 0) throw new IllegalArgumentException("non-positive value"); super(value); } } This feature doesn’t break the top-down initialization or require changes to the JVM. However, it makes initialization more flexible, thus promoting more laconic and maintainable code. JEP 454: Foreign Function & Memory API JEP 454 finalizes the Foreign Function & Memory API that enables Java programs to interoperate with code outside of the JVM safely and reliably, without the dangers of JNI. The JEP includes the following improvements: a new linker option enabling clients to pass heap segments to downcall method handles; a new Enable-Native-Access manifest attribute for JAR-files, which allows code in executable JAR files to call restricted methods without the --enable-native-access command-line option; a new possibility for clients to build C-language function descriptors programmatically; enhanced support for variable-length arrays in native memory; support for arbitrary charsets for native strings. JEP 456: Unnamed Variables & Patterns JEP 456 finalizes without changes the Unnamed Variables & Patterns feature introduced in JDK LTS 21. This functionality allows to substitute with the underscore character _ the following elements: the unnecessary type and name of a record component in pattern matching, variables that must be declared but will not be used. This increases the readability and maintainability of the Java code significantly. JEP 457: Class-File API (Preview) Class files are the cornerstone of the Java platform. Java includes several libraries for parsing, generating, and transforming class files, and frameworks usually bundle a class-file library. However, the class-file format evolves rapidly due to the six-month Java release cadence. So, the framework developers cannot keep up with the changes in the Java platform and update their class-file libraries promptly, which may lead to errors and incorrect behavior. Even Java cannot level up with the class-format changes because it uses third-party libraries for class-file processing, whose updates are also delayed. JEP 457 introduces a standard class-file API, which will evolve together with the class-file format. This way, Java components will not be dependent on third-party libraries anymore, and the framework developers will be able to support class files from new JDK versions automatically. JEP 458: Launch Multi-File Source-Code Programs JEP 458 enhances the java application launcher and enables the developers to run Java programs supplied as multiple files of source code. This will facilitate work at the initial stages of development because developers don’t have to create a project configuration for a build tool to compile several .java files, i.e., they can skip the project setup stage until they have a clear understanding of a project structure and a complex workload. This way, the traditional edit/build/run cycle shortens to edit/run. JEP 459: String Templates (Second Preview) JEP 459 is the Second Preview of the String Templates feature introduced in JDK 21. The feature enables the developers to concatenate literal text with embedded expressions, thus facilitating the expression of strings that contain values computed at run time by reducing the boilerplate code and eliminating the risks of string interpolation. The Second Preview is aimed at gathering feedback from the developers and includes only one minor change related to the types of template expressions. JEP 460: Vector API (Seventh Incubator) JEP 460 introduces the Seventh Incubator of a Vector API, which helps increase the performance of vector computations that compile to optimal vector instructions at runtime. If you would like to know more about this feature, We discussed the performance gains related to Vector API in our article dedicated to JDK 17. The Seventh Incubator encompasses several bug fixes and improvements, including the support for vector access with heap MemorySegments, backed by an array of any primitive element type. JEP 461: Stream Gatherers (Preview) The Stream API was first introduced in Java 8. With its help, streams (value sequences) can be processed sequentially or in parallel, using a range of intermediate and terminal operations: filtering, mapping, etc. The existing set of intermediate operations is rather limited, albeit efficient. However, adding more operations to the Stream API will unjustifiably complicate it. JEP 461 introduces a new feature to enhance the Stream API with custom intermediate operations so that developers can process the streams as they see fit. This is made possible with a new intermediate stream operation, Stream::gather(Gatherer), which applies special gatherers to stream elements defined by a user. Gatherers are an instance of the java.util.stream.Gatherer interface. They transform elements of a stream relying on its four functions: An initializer provides an object maintaining private state while processing stream elements. An integrator integrates a new element from the input stream A combiner can evaluate the gatherer sequentially or in parallel in case of parallel input streams. A finisher is invoked when there are no more input elements left to consume. The new feature will allow the developers to create more flexible and expressive stream pipelines, avoiding verbosity and enhancing code readability. JEP 462: Structured Concurrency (Second Preview) Structured concurrency API is aimed at improving observability and management of multi-threaded code. It is especially useful for developers working with virtual threads whose amount can reach tens of thousands. The API treats related subtasks executed in different threads as a family belonging to a single unit of work (task). Each subtask is forked and then joined in the parent task’s code block. This way, the task can coordinate subtasks and monitor them for failures, thus eliminating the common risks related to cancellation and shutdown and promoting reliability of concurrent applications. JEP 462 introduces a Second Preview of the API without any changes with the aim to gather additional feedback from developers. JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) JEP 463 introduces the Second Preview of a feature included in JDK 21 (JEP 445: Unnamed Classes and Instance Main Methods). The functionality enables the developers who just started learning Java to write simple programs and then extend them as their knowledge grows. For instance, a simple Hello World program without complicated but unnecessary for novices features will look like this: class HelloWorld { void main() { System.out.println("Hello, World!"); } } The Second Preview brings several important changes to the functionality, including the different name: Simplified classes declaration: a source file without an enclosing class declaration is said to implicitly declare a class with a name chosen by the host system. Facilitated selection of a main method to be invoked: if there is a main method with a String[] parameter, it is invoked. Otherwise, the main method without parameters is invoked. JEP 464: Scoped Values (Second Preview) Scoped values is another feature aimed at enhancing the multi-threaded programming in Java. It enables the developers to share immutable data within and across threads and should be preferred to thread-local variables as it promotes better reliability, smaller complexity, and improved footprint. JEP 464 introduces a Second Preview of scoped values without changes to gather additional feedback. Get the performance of newer JDK versions without migration! A corporate stack usually incorporates several JDK versions. While you may consider migrating newer services to the latest and greatest release, some of your services may still run on JDK 8 or 11. If you would like to boost their performance without upgrading the Java version, BellSoft engineers developed a solution for you — Liberica JDK Performance Edition, which couples JDK 11 and JVM 17. What is more, if you are already subscribed to Liberica JDK support, this functionality comes free of charge! - [A guide to using virtual threads with Spring Boot](https://bell-sw.com/blog/a-guide-to-using-virtual-threads-with-spring-boot/): We’ve been looking forward to it for a very long time — virtual threads were finalized in JDK 21! The feature is now complete and production-ready. We discussed the functionality in more detail in a dedicated article, and now, let’s see how we can use virtual threads with new Spring Boot 3.2! By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can help you save up to 30 % RAM! And did you know that you can easily reduce startup and warmup times of your Spring Boot services from minutes to milliseconds by using CRaC API for Java? Give it a try! Table of Contents Why are virtual threads so important for Java development? How to use virtual threads with Spring Boot Measuring the performance of virtual threads Why are virtual threads so important for Java development? There’s a lot of buzz around virtual threads, but are they really that beneficial for multithreaded Java applications? Virtual threads are not a magic pill for all cases of concurrent applications: everything depends on performance goals. The feature doesn’t improve latency (meaning that it doesn’t increase the speed of operation), but it is highly beneficial for high-throughput server applications that handle a lot of requests associated with blocking I/O calls. Therefore, virtual threads enable the developers to scale the application maintaining the optimal hardware utilization and preserving the traditional thread-per-request style that simplifies the writing, maintaining, and controlling multithreaded code as opposed to reactive programming. How to use virtual threads with Spring Boot For virtual threads to work, you need JDK 21 (for example, Liberica JDK, the default runtime for Spring. You can get a build for your platform here) and Spring Boot 3.2. Add the following property to the application.properties file to enable virtual threads in your application: spring.threads.virtual.enabled=true This is all you need to implement the feature. This setting works both for the embedded Tomcat and Jetty. If you use an older Spring Boot version (for instance, 3.1), you can create the following configuration to work with the embedded Tomcat: @EnableAsync @Configuration public class VirtualThreadConfig { @Bean public AsyncTaskExecutor applicationTaskExecutor() { return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); } @Bean public TomcatProtocolHandlerCustomizer protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; } } Note that virtual threads don’t substitute all standard threads magically. These settings are relevant for asynchronous operations with Spring MVC and Spring Web Flux because the framework is adapted to virtual threads. For instance, this functionality is used to process @Async methods when the @EnableAsync is enabled. Measuring the performance of virtual threads Let’s measure the performance improvement (if any) using a benchmark available on GitHub, and run it with mvn clean install. This benchmark starts up a simple application for Spring Boot 3.2 and Java 21, whose main controller upon receiving a request, waits for 300 milliseconds before giving an ID of a current thread (the GitHub code is here): public class VirtualThreadController { private static final Logger LOGGER = LoggerFactory.getLogger(VirtualThreadController.class); public static final int SLEEP_TIME = 300; @GetMapping("/") public String getResponse(){ try { TimeUnit.MILLISECONDS.sleep(SLEEP_TIME); } catch (InterruptedException e) { LOGGER.error(e.getMessage()); } long threadId = Thread.currentThread().threadId() ; return String.valueOf(threadId); } } The application performance is measured with a Gatling-based script written in Scala (the GitHub code is here): class VirtualThreadSimulation extends Simulation { before { val app = SpringApplication.run(classOf[VirtualThreadsApplication]) app.registerShutdownHook() } val httpProtocol = http .baseUrl("http://localhost:8080") .acceptHeader("application/json") .contentTypeHeader("application/json") val vtScenario = scenario("Virtual Thread Scenario").repeat(1000) { exec(http("Call the Controller") .get("/") .check(status.is(200))) } setUp( vtScenario.inject(atOnceUsers(500)) ).protocols(httpProtocol) } This script simulates the situation when 500 users send 1.000 requests to the controller simultaneously. You can set the virtual threads configuration in application.properties. The first property enables the standard configuration from Spring Boot. The second one enables our Bean with a manually configured AsyncTaskExecutor. spring.threads.virtual.enabled=true spring.threads.virtual.enabled.manually=false There’s currently no difference between these parameters, but in the future, the Spring Boot configuration may differ from the old manual configuration. You can view the results in the HTML format in the browser: Virtual threads benchmarking results These reports demonstrate that the results depend heavily on the hardware. Therefore, this test can serve as a foundation for experiments. The number may be increased or decreased depending on the capacities of your computer. For instance, meaningful results for a powerful 64-core server machine will require a bigger load than that for a laptop with 4 cores. In addition, you can utilize different load patterns. For example, you can substitute a simple sleep with real database queries or add a computation load that will use CPU resources and fill the RAM with big data. I tested the load of 500 users and 1.000 requests on Windows running on AMD Ryzen 9 3950X 16-Core Processor with 8Gb of RAM. Virtual threads gave an approx. 2-fold improvement as compared to standard threads. So, we switched one setting in Spring Boot and got a significant performance improvement “for free.” In my opinion, this qualifies as a good result. Speaking of performance gains, there’s a simple way to boost the performance of your containerized Spring Boot apps, Alpaquita Containers. Based on Liberica Lite and a lightweight Alpaquita Linux, they can reduce the memory footprint of containers by up to 30%! - [BellSoft’s Achievements in 2023](https://bell-sw.com/blog/bellsoft-s-achievements-in-2023/): As we approach the end of 2023, it’s time to pause and reflect on the incredible past twelve months we had as a team at BellSoft. This year was filled with overcoming challenges, seizing opportunities, taking part in fruitful engagements, and participating in unforgettable events. In response to the growing trend of containerization, we strive to provide the developers with a wide range of ready-to-use secure and performant container images for Java development and deployment. I’m proud to say that our efforts have yielded excellent results. Throughout the year, Liberica JDK container images were downloaded an impressive 8 million times! Apart from containers, we aim to provide our customers with the most complete Java experience based on a broad selection of solutions for Java development and deployment. And as a result, the range of our customers doubled this year! Our offerings encompass images for various Linux distributions, including Alpine Linux, Debian, and CentOS. Additionally, we provide enhanced container images with our bespoke Linux distribution — Alpaquita, a perfect alternative to Alpine, specifically designed for enterprise use. Developed and supported by the BellSoft engineers, Alpaquita celebrated its first anniversary in 2023, and it is at the core of our brand-new product — Alpaquita Containers. Introducing Alpaquita Containers Alpaquita Containers seamlessly combine the power of Alpaquita and Liberica JDK Lite, specially tailored for cloud deployments to reduce memory footprint. Alpaquita Containers were designed with efficiency in mind, so their images take up only about 40 MB. As such, our containers significantly contribute to minimizing RAM consumption by up to 30%! This underscores our commitment to providing resource-efficient solutions for modern cloud deployments. But that’s not all! This year, we’ve expanded our product family with two additional solutions. These new additionals further demonstrate our dedication to offering a diverse range of cutting-edge products that cater the evolving needs of our users. Performance Edition Line This year marked the launch of the Performance Edition line with the release of Liberica JDK 11 Performance Edition. It couples JDK 11 with JVM 17, bringing the performance of Java version 17 to JDK 11-based projects. This new solution empowers enterprises to upgrade their Java version at their own pace and boost the performance of their code with minimal to no adjustments. The overwhelming interest in Liberica JDK Performance Edition has been truly inspiring. Building on this momentum, I’m pleased to share our intention to further enrich the Performance Edition line with additional offerings in 2024. Liberica JDK with CRaC To support our partner Spring with their latest release of Spring Boot, which features baked-in support for the CRaC API, we introduced a new flavor to our flagship product — Liberica JDK with CRaC. The Coordinated Restore at Checkpoint (CRaC) API enables the developers to pause a running application and restore it later from the moment it was paused, thus achieving almost instant startup and warmup at peak performance. With Liberica JDK being the default Java runtime for the Spring Framework, we are excited to provide the Spring Boot developers with an opportunity to seamlessly integrate this innovative technology into their projects. Ongoing Commitment to OpenJDK and GraalVM In the past year ,we proudly delivered: four quarterly releases, each came with security patches and critical fixes, Notably, BellSoft is one of only three companies that provide both PSU and stabilized CPU builds. one feature release of Liberica JDK 20, and one LTS release of Liberica JDK 21. Our engineers have been actively involved in the OpenJDK community, contributing a total of 52 fixes and patches across all OpenJDK CPU releases. In addition, we took an active role in developing ParallelGC for GraalVM Community Edition. The new garbage collector implementation demonstrates promising results, with GC pause reduction ranging from 10-40%. ParallelGC is currently being integrated into Liberica NIK, and we hope that it will become a part of the GraalVM project. BellSoft’s Vibrant Engagement with Java Community in 2023 Our dialog with the Java community has been vibrant and dynamic throughout the year. Our Performance Architect, Dmitry Chuyko, has been a standout ambassador for BellSoft, delivering 30 talks at 28 global events this year. At JNation and Devoxx Belgium, Dmitry delivered two presentations, showcasing our dedication to sharing knowledge and insights with the wider Java community. Dmitry Chuyko at Devoxx Belgium 2023 Moreover, this year marks a significant milestone for the Java community: the 25th anniversary of the Java Community Process, a robust framework for the collective development of the Java platform. BellSoft, as a member of the JCP Executive Committee, was honored to be invited to the celebration held in New York. Dmitry and I were delighted to attend this milestone event in person and reconnect with other members of JCP. The JCP meeting in New York Also, we hosted the fourth episode of JRush, our web conference for Java developers. This particular episode, dedicated to modern Java development for banking and fintech, featured three experts sharing their expertise and valuable insights on building reliable and performant cloud-native Java applications. For those interested, the episode is available for free on our YouTube channel. Furthermore, our commitment to knowledge sharing extends to our blog, where we enriched the Java knowledge with 65 articles this year. Looking Ahead to 2024! As we conclude this remarkable year, I would like to extend my gratitude towards our customers, partners, developers, and the entire OpenJDK community. Your trust and collaboration have been the main reason for our success. Together, let’s ride full-speed into 2024, continuing to create a better future through technology for everyone! Our journey is just beginning, and I’m excited for the innovations and milestones that lie ahead. - [Talking Java across the globe: backstage happenings and adventures](https://bell-sw.com/blog/talking-java-across-the-globe-backstage-happenings-and-adventures/): 2023 was charged with conferences, meetings, and events dedicated to Java, some of which took place offline for the first time since the COVID-19 pandemic. And trust me, there was a lot to discuss, with some of the year’s highlights being the release of JDK LTS 21 with 15 new and improved features, including finalized virtual threads, the rise of Arm servers for enhanced application performance in the cloud, multiple solutions for increasing the performance of Java-based projects, including Coordinated Restore at Checkpoint API for minimized startup and warmup times, the release of Spring Boot 3 with baked-in support for GraalVM Native Image followed by virtual threads and CRaC support, the growing demand for OpenJDK distributions, given this year’s changes to the Oracle Java pricing model, the ongoing trend towards optimized resource usage and overall performance in Kubernetes, And many more. As such, your humble narrator attended 28 events worldwide, and in this post, I want to share the most memorable moments from my Java travels far and wide. Table of Contents Everything started in Atlanta Zoom bombing Meeting in a barn A knock on the door at the crack of dawn There’s no place like home, except for IST Retrogame-themed JNation A friendly reunion in years A nightly nipper Devoxx Belgium: a perfect closure Looking forward to 2024 Everything started in Atlanta My first American journey in 2023 started in Atlanta, where I met with Bob, a member of our Java Expert Group and blog author: together, we were bound to attend multiple JUG meetings throughout the US. A pleasant surprise was to run into Vincent Mayers, a Java Champion and organizer of Devnexus and Atlanta JUG. Atlanta I must say, I fell in love with Atlanta, although it took some time to feel the city vibe: it offers so much to see that it took us several days to touch upon the sightseeing delights it offers. And I can’t help mentioning the food in the American South, which was delicious but extraordinarily spicy and greasy. As I’m not used to getting so many calories, fat, and salt with every meal, my stomach won’t forget this journey, too, albeit for an entirely different reason! Zoom bombing After traveling south, we spent some time on the West Coast, including San Francisco, where I attended an online JUG meeting. San Francisco Much as I love the city with its sights and legendary trams, this stay was a little overshadowed by an unpleasant accident during the meeting. Just when I was giving my talk, two random blocks decided to have some fun and zoom-bombed our session! I admit that talking calmly over their howling and laughing was challenging. Alas, we didn’t manage to conduct the meeting to the end, but such happenings are the expected side effects of the digital age, I guess. Meeting in a barn One of the JUG meetings during my winter US journey was held in Charlotte, a beautiful and the biggest city in North Carolina with a magnificently sparkling skyline. Charlotte After enjoying the city attractions during the day and nightly views of the city center at night, I was, mildly speaking, surprised when we gathered the next day for the JUG session in what seemed to be a quaint barn! A JUG meeting in Charlotte But it didn’t stop us from having a great time and an engaging discussion. To paraphrase the saying: It’s the community that counts, not the location! Each such meeting means new contacts and discoveries. A knock on the door at the crack of dawn In spring, I returned to the US, but Alex, BellSoft’s CEO, accompanied me this time. Our first stop was Atlanta, where we participated in Devnexus, and then we headed to LA to attend several meetings. Los Angeles Alex was bound to fly to Singapore for a JCP meeting, so after yet another meeting, we parted ways: Alex went straight to the airport, and I hung around with friends for a while, and then went to my hotel room. Around 5 a.m., I was torn from my sleep with a loud knock on the door. “What on Earth is going on?” I thought. Crawling out of bed, I reluctantly dragged myself to the door, wishing that my rumpled, sleepy, and incredibly dissatisfied face would make a visitor regret their decision to bother me at this ungodly hour. But my dissatisfaction instantly vanished (with the rest of my sleepiness) as the visitor turned out to be none other than Alex. Apparently, his flight was delayed and then canceled altogether. After spending the night at the airport, he fell asleep almost instantly in the armchair. I wish I could say the same about myself! There’s no place like home, except for IST It would be no exaggeration to say that Istanbul, which served as the junction between Kazakhstan, the US, and the EU, became almost like my second home: I spent many a day wandering around the airport or the city waiting for my next flight. Sometimes, I even worked or slept at the airport (kudos to the engineers who invented Sleepods). Once, BellSoft’s T-shirts for giveaways traveled with me, and although they didn’t have a chance to appreciate the beauty of the Turkish capital as they were safely tucked in my suitcase. In some cases, I managed to book a connecting flight without long waiting hours between the flights. Still, as a retaliation, I was usually so exhausted when I boarded the second plane that I fell asleep even before the plane would take off. On one such occasion, I dozed off (well, to be honest, I fell dead asleep for four hours), and when I woke up, the plane didn’t move an inch, but instead, was covered in snow! “Well, that’s odd,” I thought. I guess the same thought crossed the mind of the airport employees, as no plane took off until the snow melted away. By the way, I managed to bargain a Doctor Who’s fez at the Grand Bazaar to keep up with the background theme of my Docker Who: Small Containers Through Time and Space presentation. Istanbul Retrogame-themed JNation One of the most extraordinary conferences I participated in this year was JNation, held in Coimbra, Portugal (and as the only way to get to the city is by land, I got to see amazing Lisbon where I flew to as well). By extraordinary, I don’t mean that we discussed the impact of Virtual Threads finalization in Java 21 on the gopher population in the Great Plains. It’s just that Coimbra is the home of one of the oldest universities in the world, which is a must-see per se, but seasoned with the retrogaming theme of this year’s JNation conference, the aftertaste of the event is something unforgettable. Imagine personifications of characters from our beloved video and computer games walking down the halls of the 500-year-old St. Francis Convent, and you will get my point! A cherry on the cake: I presented two topics at the conference! Portugal A friendly reunion in years Some of my warmest memories from this year’s travels are associated with the JVM Language Summit and the OpenJDK Committers Workshop at Oracle’s Santa Clara campus on August 7–9. The reason is that this event was held offline for the first time in four years due to the COVID-19 pandemic, so I was thrilled to finally meet my old friends and colleagues in person. And the memories are warm in all senses: when I was freezing under the air conditioner, Charat Changer saved me from catching a cold by giving a cozy hoodie. Moreover, in September, Alex and I attended a highly anticipated celebration of the JCP 25th anniversary in New York, so Alex managed to meet with the JCP colleagues after all. New York A nightly nipper My last US tour in 2023 was approaching its end. I went to Pennsylvania after yet another JUG meeting, and apart from the pouring rain that met me upon my arrival to Philadelphia, this episode of my US travels seemed to be going down as the most hassle-free one. I even managed to take some time off my schedule to admire the Liberty Bell. The resonating similarity between its name and the name of our company and the flagship product Liberica JDK is inspiring, don’t you think? Philadelphia You might have guessed by the name of this story that I was wrong about the hassle-free journey. I was watching peaceful dreams in my hotel room when suddenly, I was torn out of my sleep by a strange rustling noise. I opened my eyes, and when they adapted to the darkness, I deciphered a large silhouette creeping around the room. “Hey man, what are you doing here?” I grumbled. The figure stood still but didn’t make a sound or move towards the door. I felt the ”fight-or-flight” reaction kicking in and taking away the rest of my sleepiness, and as there was nowhere to flee, I decided there was nothing better to do than to try and scare the stranger away. So I stood up, and luckily, my 6.5-foot bulk of the body did its part. The man, indeed, got scared and ran away. I heaved a sigh of relief, but my joy didn’t last long: when I started to examine my belongings, I noticed that the man escaped with my smartphone! You can tell that losing the phone nowadays is worse than losing the passport. So I called the hotel security immediately and made them call the police. While the police officers were searching the building, I was trying frantically to reorganize the rest of my journey. But fortune was on my side that night: the phone was discovered because the nipper threw it away in an attempt to escape. The history is silent on whether the culprit was discovered as well, but I was so overwhelmed with happiness upon reuniting with my smartphone that I didn’t even care! Devoxx Belgium: a perfect closure My world tour finished strong in Antwerp at Devoxx Belgium in 2023. Devoxx is a large-scale event for developers, and this year was no exception: five conference days packed with more than two hundred talks and hands-on labs on the most acute topics of the modern IT environment: AI, cybernetics, security, server and cloud development, bleeding-edge tools and solutions, and of course, all things Java. Just like with JNation, it was a special joy for me to deliver two presentations, Why you need performance tests for proper Kubernetes scaling and Java on Arm (both talks are available on YouTube if you are interested in the topics). Dmitry at Devoxx Belgium 2023 Naturally, you can’t do without lively discourse with your peers and fellow developers (at Devoxx, there are even special Birds-of-Feather sessions to satisfy this need). My colleagues and I had great discussions with Java devs at our booth! Antwerp Looking forward to 2024 All in all, this year left me with only fond memories of the events I attended and the people I met. And even mishaps such as the stolen smartphone turned into amusing stories in the end. I can’t wait to see what 2024 has in stock. I plan to keep the momentum and attend as many Java conferences, meetings, and workshops as possible. Hope to see you there! - [TOP-11 articles to read to ride the crest of Java development](https://bell-sw.com/blog/top-11-articles-to-read-to-ride-the-crest-of-java-development/): A new year doesn’t necessarily mean a new beginning, especially in IT, as the trends set in 2023 are bound to gain momentum. So we prepared a list of the most popular publications in our blog, which will help you to navigate through the cutting-edge tools and practices and stay ahead of the game. Migrate from Oracle Java As Oracle took the habit of changing Java licensing and payment model every two years, enterprises consider migrating to an OpenJDK contribution to receive cost-efficient support and features absent in Oracle Java. These two articles aim to help you choose a suitable JDK vendor and migrate from Oracle without issues. Oracle Java alternatives: Comparison of OpenJDK distributions: Browse the key characteristics of the leading OpenJDK distributions to choose a perfect JDK for your project. How to migrate from Oracle JDK to OpenJDK: Follow a step-by-step guide to migrate from Oracle Java to a chosen OpenJDK distribution. Upgrade the JDK version Newer Java versions boost enhanced performance, security, and features facilitating the development process. However, upgrading the Java version may be associated with challenges. Articles below will guide you through all steps of upgrading to minimize risks of application failure and unexpected behavior. Migration from Java 8 to Java 17: Learn how to upgrade JDK 8 in an enterprise project step by step. Migration from Java 11 to Java 17: Find out how to migrate JDK-11 based projects to JDK 17 LTS with ease. Optimize cloud resources consumption Neglecting application memory footprint optimization leads to inflated cloud bills and other related troubles. These two articles provide an overview of popular solutions for reducing Java resource consumption reduction in the cloud. Distroless containers for security and size?: Find detailed answers to the most popular questions about distroless containers and decide whether they fit your needs. How to dockerize a Spring Boot app with the smallest base image: Discover an easy way to create microcontainers with Spring Boot applications without affecting the performance. Reduce Java application startup time JVM can deliver outstanding performance thanks to JIT-compilation, but as a drawback, startup and warmup can take up to several minutes, which is undesirable in certain deployment scenarios. These articles will navigate you through tips, methods, and technologies aimed at reducing the infamous Java startup/warmup times. Avoiding AWS Lambda cold starts: Discover recommendations on preventing and minimizing cold starts of Java apps in AWS Lambda. Spring Boot 3 with GraalVM Native Image: Find out about the specifics of migrating a Java app to the Native Image technology and learn how to avoid possible pitfalls. How to reduce Java application startup time: Find out methods to reduce Java application startup and warmup time to reach peak performance almost instantly. Bonus: BellSoft’s external trending publications BellSoft’s CEO Alex Belokrylov and Performance Architect Dmitry Chuyko regularly publish their articles on online platforms. Here are the top picks of their contributions in 2023 on the hottest Java topics. Buildpacks vs Dockerfiles: choose an entry point into the cloud: Learn about two most popular tools for application containerization and choose the most optimal one for your purposes. The Right Base Container Images: A Solid Foundation for Cloud-Native Java Apps: Find out how containers drive cloud-native development and how the right base image helps enterprises reach essential KPIs. Want to stay on top of Java trends in 2024? Subscribe to our newsletter and be among the first to receive news, tutorials, and recommendations on efficient Java development and deployment. - [Building cloud-native microservices with Kubernetes](https://bell-sw.com/blog/building-cloud-native-microservices-with-kubernetes/): A modern cloud offers immense opportunities to businesses in terms of growth, innovation acceleration, and cost optimization. But a resilient, flexible, easily scalable cloud-native application doesn’t fall out of the sky. We have to build it ourselves. This article will guide you through the best practices of cloud-native development. This article was inspired by Episode 4 of JRush, a series of free web conferences for Java experts. You can watch the full presentation on the topic on YouTube or register for JRush to gain access to previous episodes on software trends and cutting-edge technologies. Explore JRush Table of Contents Cloud-native microservices: advantages and challenges How to make cloud-native infrastructure deliver to your purposes Follow the best practices of building microservices Adopt a multi-cloud strategy Orchestrate containers with Kubernetes Use open-source tools for the cloud Further references: from theory to practice Cloud-native microservices: advantages and challenges Cloud native is an approach to building highly scalable applications that can be seamlessly ported to any platform, be it a private or public cloud environment or a hybrid-cloud setup that combines on-premises and cloud infrastructure. Although it is possible to port a monolithic application (developed on a single codebase) to the cloud, microservices are a better choice for long-term business strategy in most cases. They enable the developers to get the maximum out of their cloud deployment, providing Scalability: small services can be rapidly scaled out in response to traffic upsurge and just as easily scaled in. Agility: developers can integrate new features as soon as possible without interrupting overall application operation, which enables businesses to stay on top of the game and swiftly respond to customer needs. Resilience: as microservices are independent of each other, a bug or an error in one service won’t cause the whole system to crash. Tech stack flexibility: you can choose different technologies for microservices to better fit their purposes or even write the services in various programming languages. But before our microservice-based cloud infrastructure starts working to our advantage, we must build it properly. Until then, the benefits of microservices may seem more like challenges. How to make them scalable, resilient, and flexible? How to reduce time-to-market and optimize TCO instead of falling into the trap of hidden cloud costs? We will provide a few tried-and-true approaches for creating and managing cloud-native applications. If you want to get a deeper understanding of cloud cost optimization techniques and why they should be implemented as soon as possible after cloud migration, we prepared a comprehensive white paper on the topic. How to make cloud-native infrastructure deliver to your purposes Follow the best practices of building microservices Breaking down a monolith into several services is only the first step of your journey to the cloud. Given the complexity of microservice architecture, you have to be careful to avoid a tangled uncontrollable web of non-communicating components, which devour resources and developers’ time. Luckily, there are several proven methods of building lightweight, resilient services with clear connections between components and the outer world. Keep microservices small and with a minimum number of responsibilities. This will increase the processing speed and minimize the consequences of microservice failure. Separate the data storage so that microservices don’t depend on one database. As a result, you will minimize the risks related to data loss or database outage, simplify data management, and keep microservices independent from each other. Organize interservice communication with a service mesh, a decentralized infrastructure layer that handles the request delivery. A service mesh typically attaches a “sidecar” (individual proxy) to each microservice instance, which routes the traffic between services. Utilize a distributed event messaging and streaming platform for reliable asynchronous communication and processing of huge volumes of data. As a result, your microservices should look similar to this: Optimal microservices structure A key is to make microservices as independent of each other as possible so that local failures or updates don’t affect the functioning of the whole application and establish reliable communication between components. But there’s one more essential point to consider, namely the resource consumption. A containerized monolith indeed takes up several times more memory than a microservice. Still, even a microservice container image may often bloat, increasing its footprint and slowing down the speed of operations. Make your containers lightweight by choosing a small base Linux distribution (for instance, the compressed container image of Alpaquita Linux is approx. seven times as small as that of Ubuntu) and keeping unnecessary files out of the image. This way, you will accelerate development and deployment and reduce cloud storage expenses. Refer to the dedicated article for more useful tips and hands-on examples of reducing container size. Adopt a multi-cloud strategy Multi-cloud deployment means utilizing various public cloud services from several cloud providers. Cloud platforms are not created equal: cloud providers offer different services, features, rates, and billing models, so spreading workloads across multiple clouds will help you achieve Business flexibility. Clouds vary on many levels: some offer unique features, some have longer provisioning time or downtime, and some may be unavailable in specific countries. The key is to match the right workload to the right cloud environment so that the company can unleash the full potential of its application and enhance customer experience. Data sovereignty. Legislation of certain countries may prohibit data transfer from the country of origin or disclose the data to third parties, so it may be impossible to store or process data with a third-party cloud provider. Working with different cloud providers will enable businesses to meet the legislative requirements. Minimized vendor lock-in. If you rely entirely on one cloud provider, it might be difficult to migrate to another cloud platform if your provider changes terms of use, increases costs, etc. Spreading the workloads across platforms will enable you to make your application highly portable, so there won’t be any need to introduce massive changes to the infrastructure in case of migration. Cost-efficiency. Cloud providers implement different billing models (pay-as-you-go, per-subscription, etc. ) and payment periods. Service prices differ, so picking services at the most affordable rates helps companies leverage their IT budgets. Reliability. Several cloud platforms can be used for backup purposes, so if one cloud experiences a severe outage, the consequences can be mitigated by switching to another platform. Orchestrate containers with Kubernetes Let’s say you are ready to deploy your microservices on the cloud. Even if you only have a few services, managing them may be challenging. What if you scale them to thousands of containers? What if you add ten more microservices or already have fifty? How to manage these workloads without consuming the time of your engineers? In comes Kubernetes — an open-source platform that orchestrates containerized applications. Kubernetes provides functionality for automatically scaling, deploying, and managing deployed containers. What will Kubernetes do for you? Deploy the application on multiple hosts, so in case one server or even the whole cloud platform goes down, the workloads can be rapidly shifted to another environment. Create multiple instances of containers for high availability and fault tolerance. Schedule deployments and create new container instances with zero downtime. Manage containers concerning available underlying resources. Check the health (comparing the actual state to a desired state) of all containers continuously and take action to create new instances in case of failure. When you deploy containers to Kubernetes, you get a cluster. We use kubectl (a command line tool) to interact with the cluster. The cluster consists of the following components: Nodes — worker machines (virtual or physical) that run containerized applications. A master node manages the work of worker nodes and includes API Server exposes Kubernetes API and transmits all external communications to the cluster; Scheduler watches for newly created pods and assigns them to suitable nodes; Controller manager runs the controller processes (watches the current state of the cluster through API Server and makes changes to reach the desired state); Pods — groups of one or more containers with shared resources; kubelet — an agent that makes sure that containers are running and healthy; kube-proxy — a proxy that maintains network rules on nodes for network communication to pods; Service (load balancer) exposes the application to the external network and directs traffic to pods. We split an application into several microservices (you can develop services in different programming languages as they communicate with each other through language-agnostic APIs) and containerized them. All containers are then placed into pods in the Kubernetes cluster. After that, each pod can be scaled by creating a desired number of replicas. Kubernetes pods are ephemeral, meaning that they are created and destroyed dynamically to match the desired state, so a set of pods running in one moment can differ completely from a set of pods running sometime later. To eliminate communication complexity, Kubernetes uses Service API, an abstraction that exposes a group of pods over the network. A Service object defines a set of pods (endpoints) and a policy on how to make these pods accessible. All these components (and many more underlying processes and tools) aim to simplify workload management through a control loop: developers define a desired state, and Kubernetes performs all the heavy lifting to monitor the actual state and make changes to reach the desired state. Use open-source tools for the cloud Docker and Kubernetes are not the only technologies you should use with your cloud-native infrastructure. There are multiple open-source solutions aimed at facilitating microservice communication, performance monitoring, database management, and so on. For instance: Databases Apache Cassandra, a column-oriented NoSQL database with outstanding fault tolerance characteristics; Redis, an in-memory NoSQL database; MongoDB, a document store NoSQL database; MySQL, an easy-to-use relational database; PostgreSQL, an object-relational database with a wide range of features for complex queries and various data types support. Messaging/streaming platforms Apache Pulsar, a multi-tenant distributed messaging and streaming platform for cloud-native workloads; Apache Kafka, a high-throughput distributed event streaming platform; RabbitMQ, a traditional messaging platform that supports 22 programming languages. Observability platforms Prometheus, a monitoring solution that uses a custom query language for data acquisition; Grafana, an analytics and monitoring platform with great visualization capabilities. The open-source nature of these products means you can use them for free or subscribe for commercial support by selecting the most optimal solution based on the provided services and prices. Further references: from theory to practice These recommendations will help you build and manage a scalable, performant, and resilient cloud-native infrastructure. If you are ready to move from theory to practice, feel free to browse through the collection of material we prepared for developers wishing to migrate to microservice architecture and master best practices of cloud development: Create Java microservices with Spring Boot and Liberica JDK Dockerize a Spring Boot app with the smallest base image Deploy Docker container images to AWS Enhance microservice communication with a Remote Procedure Call framework Reduce the size of Docker container images Create a local Kubernetes cluster Choose an optimal Linux distribution for cloud and server use - [Liberica JDK 8u402, 11.0.22, 17.0.10, 21.0.2 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u402-11-0-22-17-0-10-21-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 8u401, 11.0.21.0.1, 17.0.9.0.1, and 21.0.1.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is only one of three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u402, 11.0.22, 17.0.10, and 21.0.2 with non-critical fixes and general improvements. The release contains 940 fixes and backports overall. BellSoft participated in eliminating 7 issues in all releases. Table of Contents How to keep your runtime secure The summary of fixes List of security issues fixed Summary of fixes in Liberica JDK Supported platforms Enjoy the most stable runtime! How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 10 security issues (CVEs) fixed. 48 total security fixes (+ 6 additional non-security fixes) in CPU release: in Liberica 6u411: 8 security fixes + 5 additional fixes; in Liberica 7u411: 7 security fixes + 1 additional fix; in Liberica 8u401: 9 security fixes; in Liberica 11.0.21.0.1: 9 security fixes; in Liberica 17.0.9.0.1: 9 security fixes; in Liberica 21.0.1.0.1: 6 security fixes. In addition, PSU releases include a total of 886 bugs and backports fixed: in Liberica 8u402: 9 security fixes (+ 3 in FX) + 28 additional fixes (+ 19 in FX); in Liberica 11.0.22: 9 security fixes (+ 3 in FX) + 178 additional fixes (+ 1 in FX); in Liberica 17.0.10: 9 security fixes (+ 3 in FX) + 280 additional fixes (+ 12 in FX); in Liberica 21.0.2: 6 security fixes (+ 3 in FX) + 295 additional fixes (+ 28 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2024-20932 7.5 security-libs java.security network low none none unchanged none high none CVE-2024-20918 7.4 hotspot compiler network high none none unchanged high high none CVE-2024-20952 7.4 security-libs java.security network high none none unchanged high high none CVE-2024-20926 5.9 core-libs javax.script network high none none unchanged high none none CVE-2024-20919 5.9 hotspot runtime network high none none unchanged none high none CVE-2024-20921 5.9 hotspot compiler network high none none unchanged high none none CVE-2024-20945 4.7 security-libs javax.xml.crypto local high low none unchanged high none none CVE-2024-20925 3.1 javafx media network high none none unchanged high high none CVE-2024-20923 3.1 javafx graphics network high none required unchanged low none none CVE-2024-20922 2.5 javafx network-toolkit local high none required unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 CVE-2024-20932 • CVE-2024-20918 • • • • CVE-2024-20952 • • • • CVE-2024-20926 • • CVE-2024-20919 • • • • CVE-2024-20921 • • • • CVE-2024-20945 • • • • CVE-2024-20925 • • • • CVE-2024-20923 • • • • CVE-2024-20922 • • • • Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head to Liberica Download Center. Download Liberica JDK - [Liberica Native Image Kit 23.0.3 and 23.1.2 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-3-and-23-1-2-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.3 for JDK 17 and 23.1.2 for JDK 21 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable improvements Better support for AWT and JavaFX fullscreen mode. Intrinsified memory copying routines on AMD64 platforms. Where available, they now use AVX instructions for better performance. Improved SubstrateVM monitor enter/exit routines for accelerated startup of native images. Head to this article for more details on the improvement. List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2024-20932 7.5 security-libs java.security network low none none unchanged none high none CVE-2024-20918 7.4 hotspot compiler network high none none unchanged high high none CVE-2024-20952 7.4 security-libs java.security network high none none unchanged high high none CVE-2024-20926 5.9 core-libs javax.script network high none none unchanged high none none CVE-2024-20919 5.9 hotspot runtime network high none none unchanged none high none CVE-2024-20921 5.9 hotspot compiler network high none none unchanged high none none CVE-2024-20945 4.7 security-libs javax.xml.crypto local high low none unchanged high none none CVE-2024-20925 3.1 javafx media network high none none unchanged high high none CVE-2024-20923 3.1 javafx graphics network high none required unchanged low none none CVE-2024-20922 2.5 javafx network-toolkit local high none required unchanged none low none Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [A novel GraalVM optimization for faster native images startup](https://bell-sw.com/blog/a-novel-graalvm-optimization-for-faster-native-images-startup/): Native images generated with GraalVM native-image compiler already start up in less than a second. But the BellSoft engineers added a new optimization to SubstrateVM, which enables native images of multithreaded applications to start up even faster! The essence of the change: improved monitor enter/exit routines in SubstrateVM We improved the way monitor enter/exit routines are executed in SubstrateVM, which lies at the heart of GraalVM Native Image. Monitor enter happens when, for example, a synchronized block is about to be executed. Take a look at the following code snippet: synchronized (obj) { ... } The first line states that the monitor associated with the object obj is entered. Upon exit from the synchronized block, the monitor is said to be exited. When a monitor associated with an object is entered, a lock object is read from the object header. Then, an identifier of the current thread is written into it to indicate that the lock is acquired by the current thread. As several threads can compete to enter the monitor, we write the owner thread ID using compare-and-swap (CAS) operation to ensure that only one thread can succeed in acquiring the lock, and other threads lose. Lock objects are created lazily. This makes sense, since only few objects in a program are ever used for synchronization. When we synchronize on an object for the first time, the lock object has to be created. But in this case, we know it is not shared with other threads, since we have just created it, so we don't have to use CAS to write thread ID into it. Another optimization occurs when a monitor is exited. SubstrateVM guarantees that monitor entries and exits are properly ordered, which means we never exit a monitor that has not been previously entered. When a monitor is exited, we can be sure that a lock object exists, and simply read it from the object header. As a result, we can skip calling into the generic lock acquisition method which checks whether the lock exists and creates a new one if it does not. Optimization results We tested two Liberica NIK builds, with and without the improvement. For our study, we used the Spring Petclinic demo on Ubuntu Linux, Intel 8 core CPU. The first test represents a non-contended case as it is single-threaded and reads data using various combinations of input streams, which is a synchronized operation according to the specification. The results demonstrated a 34% improvement as follows: Reading data using various combinations of input streams On the contrary, the second test represents the contended scenario where a specific number of threads was started that called the same synchronized method repeatedly. The results with optimized and unoptimized builds are shown below. Calling a synchronized method repeatedly Thanks to the optimization, the application startup time improved by 3.3%. The patch will be integrated into all GraalVM distributions soon The improvement has already been merged to GraalVM master branch, and it is available in all Liberica Native Image Kit (NIK) builds starting January 2024. Subscribe to our newsletter and don’t miss the information about the latest GraalVM enhancements! - [BellSoft releases Liberica JDK 21 for RISC-V with support](https://bell-sw.com/blog/bellsoft-releases-liberica-jdk-21-for-risc-v-with-support/): RISC-V, a novel processor architecture, is steadily gaining popularity. Thanks to its high performance, flexibility, and open-source nature, it is a viable alternative to Intel and ARM, used in more and more industries every year, from IoT to smartphones, automotive, HPC, and many others. The total market for RISC-V SoCs is forecasted to reach $92.7B by 2030, representing a 47% annual growth rate. BellSoft is dedicated to providing developers and enterprises with the most complete Java experience. Our Liberica JDK already supports the widest range of system configurations on the market compared to other vendors. And now, we are happy to announce the release of Liberica JDK builds with RISC-V support! Download Liberica JDK for RISC-V The state of Java for RISC-V In our previous article, we discussed the grounds for RISC-V’s rapid expansion, but what does Java have to do with the embedded systems where RISC-V is primarily used? Indeed, Java is perceived as the programming language used chiefly for enterprise applications, but it also has its share of the embedded market, the key reasons being: High performance and small memory footprint, especially when using a dedicated JDK build, Great portability thanks to the WORA (write once run anywhere), so there’s no need to rewrite an application when introducing a new architecture in production, Numerous standard libraries for any task keep the developers from writing their implementation for a particular use case, Convenient in-built memory management, helping to avoid errors related to memory allocation (common for C/C++). Therefore, a Java runtime with RISC-V support will be a valuable addition to the tool arsenal of companies wishing to benefit from this novel ISA. Great news is that the OpenJDK port on the Linux/RISC-V platform has already been integrated into JDK 19. Although RISC-V and ARM are different architectures, the new port is similar to the aarch64 port, with which BellSoft gained vast experience by introducing optimizations within JEP 315. Nevertheless, even with the port available, you need to correctly build and test the JDK binary to use it with RISC-V, which is a complicated and time-consuming procedure. Several Linux distributions offer packages for RISC-V, but until now, no OpenJDK vendor has released tested and supported OpenJDK 21 builds for RISC-V. BellSoft has provided Liberica JDK tailored to ARM-based embedded systems for several years. We decided to expand the line of supported system configurations with this architecture to satisfy the growing demand for developing applications that run seamlessly on RISC-V. Liberica JDK builds are fully compatible with RISC-V, so your existing or to-be-developed applications can be ported without issues. Download Liberica JDK for RISC-V now! Liberica JDK builds for Linux on RISC-V are available for JDK 21 LTS: BellSoft commits to supporting version 21 until 2032, providing users with all the benefits of an open-source Java runtime from a leading OpenJDK contributor. This includes quarterly CPU/PSU updates and additional solutions for Java development. The RISC-V support in Liberica JDK comes with a Standard JDK flavor, offering three VMs: Server VM, Client VM, and Minimal VM designed specifically for systems with lower performance so that your Java applications start up faster and utilize less memory. Liberica JDK is free for personal and commercial use. For those requiring enterprise-level support for the Java runtime, BellSoft offers flexible support plans featuring 24/7 service directly from the Java engineers with over 20 years of experience working with OpenJDK. Moreover, we leverage our expertise when building and testing the builds, and we are ready to promptly resolve issues you encounter with the runtime. Contact us, and we will be happy to answer all your questions! Download Liberica JDK for RISC-V - [BellSoft releases Liberica JDK 21 for RISC-V with support](https://bell-sw.com/news/bellsoft-releases-liberica-jdk-21-for-risc-v-with-support/): SAN JOSE, Calif., Jan. 30, 2024 /PRNewswire/ -- RISC-V is a free, open RISC instruction set architecture (ISA) that is currently gaining popularity due to its high performance, flexibility, and cost-efficiency. RISC-V advantages are evident to many industries, from IoT to servers. Many companies in these industries use Java as their default programming language, and they are seeking to employ Java on RISC-V. BellSoft aims to be the first in the service and convenience of your Java journey, bringing the advantages of modern technologies to you as soon as they become available. We now have tailored Liberica JDK builds for RISC-V, so you can port your applications without any issue. Java on RISC-V RISC-V is a viable alternative to Intel and ARM, with the total market for RISC-V SoCs predicted to reach $92.7B by 2030. In response to the increasing availability of RISC-V hardware, the OpenJDK port on the Linux/RISC-V platform has already been integrated into JDK 19, but developers need to build a JDK binary the right way for use with RISC-V. Liberica JDK for RISC-V: is fully TCK-compliant; brings you the freedom of WORA (write once run anywhere); enhances the family of BellSoft products, allowing you to work with the same vendor for every development goal on all platforms and for all versions. Liberica JDK builds for Linux on RISC-V are available starting from JDK 21 LTS. BellSoft is committed to supporting version 21 until 2032. Liberica JDK Standard is a base Liberica JDK flavor with three VMs and all garbage collectors, designed specifically for running Java on RISC-V in a highly performant and cost-efficient way. About BellSoft BellSoft delivers the most complete Java experience with a more secure, reliable, and cost-effective approach to application development on any platform and in any environment. BellSoft is one of the leading contributors to the OpenJDK, and the only vendor that supports current LTS Java versions, legacy JDK 6 & 7 and Liberica NIK. Liberica JDK is the runtime of choice for VMware, Spring Framework, JetBrains, and millions of users worldwide. For more information, visit www.bell-sw.com. - [What is Quarkus](https://bell-sw.com/blog/what-is-quarkus/): There are multiple Java frameworks for developing Java applications: Spring Boot, Micronaut, MicroProfile, Javalin, etc., each with their own strengths. Quarkus is another Java framework tailored to developing cloud-native Java microservices and promising to optimize memory footprint and the startup time of application. In this article, I won’t delve into Quarkus startup study, but rather evaluate the footprint of Quarkus containers created with the default settings and via buildpacks, and see if I can optimize the size of a resulting container image even more. Table of Contents Overview of the Quarkus framework Quarkus tutorial Create your first Quarkus application How to build Quarkus container images Conclusion Overview of the Quarkus framework Quarkus is an open-source stack of technologies with MicroProfile support aimed at optimizing Jakarta EE for building microservices. It is oriented to Kubernetes and adapted to OpenJDK HotSpot and GraalVM. To create microservices, Quarkus implements Eclipse MicroProfile APIs along with other valuable tools, such as Apache Kafka, Camel, dependency injection, Hibernate ORM (JPA and JTA annotations), RESTEasy (JAX-RS), etc. It also provides Maven and Gradle plugins so that you can run your application in development mode on a local or remote machine. In addition, Quarkus supports GraalVM, which lets the developers use ahead-of-time (AOT) compilation to convert the bytecode into native machine code. Another Java framework with built-in support for GraalVM Native Image is Spring Boot. With GraalVM, apps can be compiled to native executables with no need for dynamic scanning and loading all classes into a JVM. By default, all classes are initialized at build time. As a result, the application starts much faster and consumes significantly less resources. Сompliance with Java EE standards enables developers to use existing APIs and run their applications in an optimized runtime without rewriting them. Basically, you can use enterprise APIs and extend them according to your requirements (for example, use Quarkus extensions for reactive messaging, Vert.x, or Camel). Quarkus tutorial Create your first Quarkus application If you are familiar with Spring Boot, you’ve already used the Spring Initializr to quickly whip up a bare Spring Boot application with the required dependencies. Quarkus offers a similar starting point, code.quarkus.io. Let’s go there and adjust some basic settings. Choose Choose Java version 17 and build tool maven, Change the group and artifact ID to your liking or keep the defaults, Select the extensions: RESTEasy Classic and RESTEasy Classic's REST Client. There are dozens of additional extensions, including those for cloud services, security, messagings, data handling and so on. But we’ll keep it simple for demo purposes. Press Generate your application and open the project in your favorite IDE. The application contains only one class, GreetingResource, with the following content: import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello RESTEasy"; } } Open the Terminal and run this command to build your application skipping the tests: ./mvnw package quarkus:dev -Dmaven.test.skip=true -Dquarkus.test.continuous-testing=disabled The REST endpoint is exposed at localhost:8080/hello, so open the page in your browser to see your app’s “Hello RESTEasy” message. Without stopping the app, you can now introduce changes to your project (for instance, substitute the default message with ”Hello from Quarkus”). Refresh the browser page, and you’ll see a new greeting. This is made possible by the in-build live coding functionality, thanks to which you don’t have to recompile the app and wait for several seconds for it to start every time you make changes. How to build Quarkus container images Containerize the Quarkus app with a Dockerfile As Quarkus offers the “containers-first” approach, let’s see how we can containerize our application. You can use the traditional approach and configure the container manually with a Dockerfile. Run ./mvnw -Dmaven.test.skip package After that, go to the docker directory of your Quarkus project. There are several ready Dockerfiles, we need the Dockerfile.jvm one. It uses Red Hat Ubi 8 and OpenJDK 17 as a base image. Place the file to the root directory and run docker build -f Dockerfile.jvm -t quarkus-red-hat . Now, check the image with: docker images quarkus-red-hat latest 04aaf8eb792b About a minute ago 485MB The resulting container seems to be too heavy. Can we do better? Red Hat offers OpenJDK 17 runtime images on UBI8 without JDK tools, the compiler, or Maven. Let’s take this image and see what we get. Substitute the FROM line in the Dockerfile with: FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18 After that, run docker build -f Dockerfile.jvm -t quarkus-red-hat-runtime . Check the images: docker images quarkus-red-hat-runtime latest daf6550b5f74 14 seconds ago 444MB That’s better. But can we optimize the footprint even more? Return to the Dockerfile and substitute the FROM line with: FROM bellsoft/liberica-runtime-container:jre-17-stream-musl Here, we take advantage of Liberica Runtime Container with Liberica JRE Lite and Alpaquita Linux to create a lightweight container image with your application. Run docker build -f Dockerfile.jvm -t quarkus-liberica-runtime-container . And verify the image with docker images quarkus-liberica-runtime-container latest 0f6b58dc07f0 4 seconds ago 135MB As you can see, the resulting image is 3.5 times smaller compared to the first one we made! Containerize the Quarkus app with buildpacks There’s another approach to building container images — the buildpacks. If you are unfamiliar with the technology, refer to our previous guide. In short, buildpacks facilitate the development with automatic containerization. There’s no need to write a Dockerfile: run one command, and the buildpack will scan the project and turn it into a production-ready image. The Quarkus project offers its own buildpack for building Java and Native Image containers. Under the hood, it uses Paketo buildpacks that utilize Liberica JDK by default, so the resulting image will be based on this OpenJDK distribution. To containerize our app this way, we need to add the buildpack extension. Run ./mvnw quarkus:add-extension -Dextensions='container-image-buildpack' And then ./mvnw install -Dquarkus.container-image.build=true -Dmaven.test.skip . . . Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk' Adding cache layer 'paketo-buildpacks/syft:syft' Adding cache layer 'paketo-buildpacks/maven:application' Adding cache layer 'paketo-buildpacks/maven:cache' [INFO] Buildpack build complete, with exit code 0 [INFO] [io.quarkus.container.image.buildpack.deployment.BuildpackProcessor] Buildpack build complete [INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 198821ms This will successfully build a container image of your Quarkus project. Verify that the image was created (the name of your image will be different depending on your Docker ID): docker images catherineedelveis//code-with-quarkus 1.0.0-SNAPSHOT c6a436460a70 3 minutes ago 255MB As you can see, buildpacks help to automate the containerization process, but you don’t have the control over the image insides, so it may be bigger than the one created manually. Conclusion Quarkus is steadily gaining popularity among developers used to working with Jakarta EE standards. For others, the learning curve may be longer. As far as container images are concerned, there are two alternatives: Buildpacks that automate the containerization process but take away control over the image layers; Dockerfiles that can be adjusted to reduce the container image footprint. Manual configuration takes longer, but the performance gains can be significant with the lightweight base image and additional techniques. - [How to use a Dockerfile linter](https://bell-sw.com/blog/how-to-use-a-dockerfile-linter/): Developers should follow the best practices of compiling Dockerfiles stated in the official Docker documentation to avoid issues with container images such as: failures when building images, inefficient images, security gaps. However, it is often difficult to assess the quality of your Dockerfile in practice as it may contain numerous complicated instructions. This is where a linter comes to our rescue and verifies the Dockerfile against the defined best practices. Let’s browse the available linting tools and learn how to assess and edit our Dockerfiles on the fly. Table of Contents An overview of Dockerfile linters Hadolint dockerfile-lint dockerfile_validator How to lint Dockerfiles with Hadolint from the command line Install Hadolint Rules and severity levels Hadolint configuration file Finally: Lint the Dockerfile Common Dockerfile linting errors and how to solve them DL3019: Remove the apk cache after installing packages DL3018: Pin the package version DL3059: Avoid multiple consecutive RUN instructions DL3015: Avoid installing additional packages Conclusion An overview of Dockerfile linters A linter is a tool evaluating the code quality. For instance, some of them are aimed at assessing the quality of Java code — you probably use one of those when compiling the application. IDE hints are also an example of code linting. As Dockerfile is a program written in a specialized language that describes the process of building an OCI image, it can also be evaluated by a linter. In the case of Dockerfiles, you may perceive a linter as an editor proofreading the author’s text. There are multiple Dockerfile linters on the market, some of them are commercial or bundled with a vendor’s products as a supplement. But I will provide a summary of free solutions. Hadolint Hadolint is the most popular open-source Dockerfile linter written in Haskell. After receiving a Dockerfile, the tool checks it against the set of rules (more on which is in the section below). One of distinguishing Hadolint features is the usage ShellCheck, another open-source tool that lints bash and sh code included into the RUN instructions. In addition, Hadolint can be used for Dockerfile LABEL linting (LABEL instructions are used to add metadata to the image). The tool is compatible with macOS, Windows, and Linux. It is also available as a container image, so you can run it inside the container without installing it locally. In addition, Hadolint integrates with IDEs and many CI tools, which makes it an optimal choice for corporate development. dockerfile-lint Another open-source linter is dockerfile-lint written in JavaScript. Just like Hadolint, it checks the Dockerfile against a set of rules and can also be used for LABEL linting. The results can be viewed in JSON or XML. You can install the tool and run it locally on your PC or clone the GitHub repository and run the tool from the bin directory without installing it. dockerfile_validator dockerfile_validator is an open-source extension for Visual Studio Code based on dockerfile-lint. The tool checks the Dockerfile against the default and custom rule files and can also be used to evaluate LABEL rules. How to lint Dockerfiles with Hadolint from the command line Install Hadolint There’s a variety of ways to use Hadolint. You can install it locally or run the Hadolint Docker container. In the case of macOS or Linux, the easiest way to install Hadolint is to get it via brew: brew install hadolint For Windows, you can use scoop: scoop install hadolint You can also download Hadolint from the latest release page. If you want to use a container image, you can pull it with: docker pull hadolint/hadolint Or if you need a container with shell access, you cann get a container for Debian or Alpine: docker pull hadolint/hadolint:latest-alpine Run the following command to verify the installation: hadolint --version Haskell Dockerfile Linter 2.12.0 Rules and severity levels Hadolint issues the result of assessment in the following format: : The meaning of LINE_NUMBER and DESCRIPTION is clear, but what about RULE_CODE and SEVERITY_LEVEL? The rule code comprises a prefix (DL meaning that it comes from Hadolint or SC from SpellCheck) and the rule number. The list of rules can be found here. Note that this list is actively updated, so you should keep up with the latest version. The severity level indicated how critical the violation of the rule is. There are five severity levels: IGNORE STYLE INFO WARNING ERROR In addition, a rule code can have no severity level. You can ignore certain rules, add custom rules, or change the severity level with command line flags or via the configuration file. Hadolint configuration file The configuration file can be applied globally or to a specific project, which is much more convenient that passing command-line options each time you want to check a Dockerfile. The config file should be written in yaml format. Let’s take a look at the following example and break it up line by line: failure-threshold: warning format: json ignored: - DL3001 override: warning: - DL3059 trustedRegistries: - docker.io failure-threshold: warning sets the threshold for severity levels. Only the rules with the severity level above threshold will cause a failure. format: json specifies the format of the output file. ignored: DL3001 tells the linter to not check against rule DL3001 when assessing the Dockerfile. override: warning: DL3059 upgrades the severity level of the specified rule from INFO to WARNING. trustedRegistries: docker.io specifies the registries from where images should be pulled; otherwise, the linter issues a warning. For instance, if your enterprise receives JDK from a vendor, including the vendor's image registry on the list will prevent anyone from pulling a JDK from another source. You can place the configuration file into root of the project, or, if it is located elsewhere, set the path with hadolint --config /path/to/config.yaml Dockerfile Finally: Lint the Dockerfile Alright, it’s time for the most interesting part: let’s feed a Dockerfile to Hadolint and see what it tells us. For this purpose, you can copy and save a ‘bad’ Dockerfile below or use your own: FROM bellsoft/liberica-runtime-container:jdk-17-stream-musl as builder WORKDIR /home/myapp ADD demo /home/myapp/demo RUN cd docker-image-demo && ./mvnw package FROM bellsoft/alpaquita-linux-base:latest RUN addgroup -S spring RUN adduser -S spring -G spring USER spring:spring VOLUME /tmp WORKDIR /home/myapp COPY --from=builder /home/myapp/demo/target . EXPOSE 8080 CMD ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"] I won’t apply the configuration file included in the section above so we can get the output as is. Go to the directory where the Dockerfile is located and run hadolint Dockerfile Or, if you are using a container image: docker run --rm -i hadolint/hadolint < Dockerfile If you used the Dockerfile above, you will get the following result: Dockerfile:3 DL3020 error: Use COPY instead of ADD for files and folders Dockerfile:4 DL3003 warning: Use WORKDIR to switch to a directory Dockerfile:5 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag Dockerfile:7 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation As you can see, all Hadolint remarks are to the purpose. You can now go through the Dockerfile and fix the mistakes accordingly. Common Dockerfile linting errors and how to solve them In this section, I’d like to summarize the most common errors discovered by Hadolint and ways to quickly remedy them. I will use Liberica Runtime Container based on Liberica JDK Lite and Alpaquita Linux as an example. DL3019: Remove the apk cache after installing packages Alpaquita Linux contains only the essential packages, thanks to which it is much smaller than other popular Linux distros. The DL3019 issue may arise when you install the additional packages without cleaning the cache, which will be added as a layer to the final image. So using this command in the Dockerfile RUN apk add liberica17-lite-jdk-all Will result in the following message from Hadolint: DL3019 info: Use the `--no-cache` switch to avoid the need to use `--update` and remove `/var/cache/apk/*` when done installing packages Therefore, the following instruction will solve this issue: RUN apk add --no-cache liberica17-lite-jdk-all DL3018: Pin the package version The instruction above will yield one more error, namely DL3018 warning: Pin versions in apk add. Instead of `apk add ` use `apk add =` If you don’t specify the version, the package manager will get the latest one, which may result in application failures. For enhanced stability in production, it is recommended to use a particular package version. The remediation will be to add the package version, for instance: RUN apk add --no-cache liberica17-lite-jdk-all=17.0.10 DL3059: Avoid multiple consecutive RUN instructions Each RUN instruction, as well as COPY and ADD, adds a layer to the container, which may result in a bloated final image. So it is better to combine instructions in one if they follow each other. Therefore, instead of RUN cd my-application RUN ./mvnw package You should write RUN cd my-application && ./mvnw package DL3015: Avoid installing additional packages This issue doesn’t affect Alpaquita and Alpine because they use the APK package manager that doesn’t install recommended additional packages. But APT does, so your container image will be stuffed with packages you actually may not use. Therefore, instead of RUN apt-get update && \ apt-get install -y openjdk-17-jdk You should use RUN apt-get update && \ apt-get install -y openjdk-17-jdk --no-install-recommends && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* Note that we also clean the cache of the apt-get lists here. Conclusion As you can see, even if you switch to a lightweight Liberica Runtime Container and apply other viable Docker image reduction techniques, you should keep your Dockerfiles neat to avoid stuffing them with unnesessary components and comply with the standards. And a Dockerfile linter is a highly useful tool in this regard. You can even integrate a linter into the CI pipeline (for instance, there’s a GitHubAction for Hadolint), and your container images will always be efficient and secure. - [How to install Linux on a mac with Apple silicon](https://bell-sw.com/blog/how-to-install-linux-on-a-mac-with-apple-silicon/): Apple steadily transitions its machines to Apple silicon. The performance of these chips is impressive as we discussed in our previous article, and for rare cases of software incompatibility, there’s Rosetta. But what if you need to run Linux on an M-series mac? For instance, you work with containers, and Docker lacks certain features on Apple Silicon. Another incentive would be to use novel features such as Coordinated Restore at Checkpoint (CRaC) API with new Spring Boot to slash startup and warmup times of Java applications from seconds to milliseconds — right now, this tool works with Linux only. Rosetta won’t help in those cases, so you need to find a way to install a Linux distro. In this article, I’ll go through the options of running foreign OSs on Apple silicon and provide a step-by-step guide to installing a Linux distro on M1 mac. Table of Contents How to run Linux on a mac with Apple silicon? Virtualization Emulation An overview of virtual machines for M-series macs Parallels VMware Fusion UTM VirtualBox Installing Linux VM on an Apple silicon mac with UTM How to select a Linux distribution Install Ubuntu Desktop Install Linux for Server Install Liberica JDK and Liberica Native Image Kit on Alpine Linux Conclusion How to run Linux on a mac with Apple silicon? What are the options of installing and running Linux on ARM-based macs? It would be perfect if we could run Linux on Apple silicon natively, and the process is on-going. There’s, for instance, a community-based Asahi Linux aimed at porting Linux to M-series macs, so we may see some inspiring results in the future. Meanwhile, the developers have two viable solutions for coupling Linux with Apple silicon, namely, virtual machines and emulators. Virtualization The most convenient way to utilize software in a foreign environment is by means of virtualization. Virtualization tools use Apple’s hardware support to divide hardware resources into several parts, enabling multiple virtual machines (VMs) to run on one physical computer. Software running in these VMs, including the OS, utilizes them as if they were real computers. Virtualization is a resource-efficient and performant way of running ARM-based Linux on Apple silicon. But it is unsuitable for x86_64 distributions, and, in addition, you need an out-of-the-box solution for setting up a VM on M-based macs. Emulation Another option of running Linux on Apple silicon is through emulation. Contrary to virtualization, emulation tools such as QEMU do not partition the physical resources, but mimic the entire hardware by processing each instruction programmatically. As a result, your selected distro (for ARM or x86_64) “believes” it is running in a native environment. However, emulation has several cons. Due to the fact that emulators act as translators for foreign software, it is very resource-expensive and may affect the performance. An overview of virtual machines for M-series macs First thing first: you don’t have to use third-party virtualization solutions because you can build a VM yourself using Xcode and Swift. If you don’t want to go to that trouble, consider ready frameworks described below. Parallels Parallels Desktop is a virtualization solution powered by Windows and aimed primarily at seamlessly running Windows on macs (including M-series chips). However, you can also install a Linux distribution through Parallels. The tool offers a convenient user-friendly interface and enables a simple installation of a foreign OS on Apple Silicon with just a few clicks. Note the Parallel Desktop is commercial software (with a no-charge trial period of 14 days) with enterprise-grade support, so if you need a virtual machine for personal use, you may want to consider other free options. VMware Fusion VMware Fusion is a desktop hypervisor developed by VMware to run foreign OSs or other tools such as Kubernetes in virtual machines on Apple Silicon or Intel macs. You can create multiple virtual machines on your mac and even play 3D games in the Linux VM as Fusion provides 3D hardware acceleration via OpenGL 4.3. The VMware Fusion Pro (available with commercial license) version includes additional features, such as connecting to remote vSphere/ESXi servers or simulating virtual networks. If you don’t need these capabilities, you can download and use VMware Fusion Player for free after registering for a personal use license. UTM UTM is an open-source solution created specifically for macOS. It enables the developers to run ARM64-based systems on M-series macs at near native speed using Apple hypervisor. In addition, you can run x86_64 OSs on Apple Silicon or ARM64 systems on Intel-based macs via emulation. UTM has a nice GUI and is easy to install and use. And if you wish, you can experiment with other emulated processors provided by UTM, including the novel RISC-V. UTM is free to use, but there’s also a paid version of UTM in the App Store. The only difference from the free package is automatic updates. VirtualBox VirtualBox developed by Oracle is an open-source virtualization tool free for noncommercial use. VirtualBox Enterprise Edition for businesses is also available. The solution is targeted to x86 and AMD64/Intel64 platforms. There’s a VirtualBox 7 package for M-series macs, but the problem is that this build is in beta-mode and will remain so. The Oracle’s support team stated that “There won't be any further M1 packages on the 7.0 branch as we don't backport any significant fixes/enhancements for ARM there.” You can download the testbuilds here. If performance and stable operation are critical to you, VirtualBox may not be the best choice in this case. Installing Linux VM on an Apple silicon mac with UTM How to select a Linux distribution First of all, download the ISO bundle of a preferred Linux distribution. Note that not all Linux distros work with Apple Silicon, the reason being the difference between page sizes. M-series macs are compatible with pages of 4kb and 16 kb only. For instance, RHEL 8.x can’t be installed on Apple Silicon, although it has an ARM64 build. Therefore, verify that Linux is compatible with your mac before downloading it. I’d recommend creating two virtual machines, one for Linux Desktop and another for Linux Server. A VM with Linux Desktop provides the familiar GUI, but takes up more space on disk, and a Linux Server VM is most suitable for corporate workflow. For instance, you can take Ubuntu, the most popular distro, for desktop use and lightweight Alpine or Alpaquita for server workflow. Alpine and Alpaquita contain only the most essential packages (others can be easily pulled from the repository) and are based on musl libc, so they take only a few Mb on disc. Alpaquita is the only Linux optimized for Java, so if you deploy Java apps to the cloud, take a look at Alpaquita Containers that may help you reduce RAM consumption is the cloud by up to 30%. Consequently, you need to download two ISO packages to follow this guide step-by-step (alternatively, create one VM fitting your purposes): The latest LTS version of Ubuntu Desktop, 64-bit ARM (ARMv8/AArch64), Alpaquita Linux for Aarch64 or Alpine Linux Virtual for Aarch64. Now, download UTM. You can get the package from the site or App Store. In the latter case, you’ll have to buy it. Install UTM like any other software and open the application. Starting with UTM Install Ubuntu Desktop Click Create a New Virtual Machine. After that, choose Virtualize, and then select Linux. The following window will appear. Booting Linux ISO image Click on the Browse… next to the Boot ISO Image option and navigate to the folder where you downloaded the Linux ISO images. We’re starting with Ubuntu Desktop, so boot this image. On the next screen, the program will ask you to configure the hardware resources for your Linux VM. The best practice is to allocate at least 2GB RAM and 50% of cores (in my care, 4 CPUs) to the virtual machine for it to function correctly. Configuring hardware resources Next, specify the amount of storage. The number depends on the type of Linux (Ubuntu Desktop requires at least 15GB, Alpine Virtual can do with 100MB) and your purposes: what you are going to store and do. I gave my VM 20GB. On the next screen, click Browse… to select a folder that will be accessible to your VM. Assigning a directory for VM The final screen provides a summary of your settings. Verify that everything is correct (you can also change the name of your VM here) and press Save. Congratulations! You can now launch your VM. VM main page Press the play button in the upper right corner. The VM will start, and then proceed to the usual Linux installation. Ubuntu Desktop VM Install Linux for Server The first steps up to booting the ISO image are the same as with Ubuntu. Let’s proceed with allocating hardware. You can give your VM the same amount you gave Ubuntu (2GB), but I’m going to work with native images. Native image compilation requires much more memory, so I would recommend allocating at least 8GB of RAM if you’d like to utilize this technology in your project. In addition, allocate four CPU Cores. Assigning hardware resources to Server VM As far as the storage is concerned, Alpine Virtual or Alpaquita require approx. 100 MB, so the actual amount of allocated disc memory depends on your needs. I gave my VM 8GB. Assigning storage Then, select a shared directory. Verify the settings on the next screen and press Save. After that, start your VM and proceed with the OS installation. You can find the instructions on setting up Alpaquita VM here. When setting up Alpaquita VM, you can select the JDK and additional tooling that will be installed automatically. The default login for Alpaquita is alpaquita and the password is alpaquita. The default login for Alpine is root and the password is empty. Alpine Server VM Install Liberica JDK and Liberica Native Image Kit on Alpine Linux After setting up the Alpine VM, you can populate it with all the tools you need. Here, I’ll provide a quick guide on installing Liberica JDK and Liberica NIK that have dedicated builds for Alpine on ARM. Let’s start with Liberica JDK. You will require sudo, and as it is not provided with Alpine by default, install it first: $ apk update $ apk add sudo Now, set up the Alpine Linux Repository and add the key: $ echo "https://apk.bell-sw.com/main" | sudo tee -a /etc/apk/repositories $ sudo wget -P /etc/apk/keys/ https://apk.bell-sw.com/info@bell-sw.com-5fea454e.rsa.pub Next, pull the required Liberica JDK version from the repository: $ apk add bellsoft-java21 Verify the installation by checking the JDK version: $ java -version openjdk version "21.0.2" 2024-01-16 LTS OpenJDK Runtime Environment (build 21.0.2+14-LTS) OpenJDK 64-Bit Server VM (build 21.0.2+14-LTS, mixed mode, sharing) The next step is to install Liberica Native Image Kit. First, go to the Liberica Native Image Kit Download Center and select the version you want (there are Liberica NIK versions for JDK 17 and 21). Next, download the .apk archive to your Alpine VM: $ wget https://download.bell-sw.com/vm/23.1.2/bellsoft-liberica-vm-core-openjdk21.0.2+14-23.1.2+1-linux-aarch64-musl.apk As we have already added the public key when setting up Liberica JDK, you can proceed with installing the Liberica NIK package with $ sudo apk add ./bellsoft-liberica-vm-core-openjdk21.0.2+14-23.1.2+1-linux-aarch64-musl.apk After that, check the path to the NIK installation: $ apk info -L bellsoft-liberica-vm-core-openjdk21 bellsoft-liberica-vm-core-openjdk21-23.1.2-r0 contains: opt/bellsoft/liberica-vm-core-23.1.2-openjdk21/LICENSE_NATIVEIMAGE.txt opt/bellsoft/liberica-vm-core-23.1.2-openjdk21/release opt/bellsoft/liberica-vm-core-23.1.2-openjdk21/readme.txt opt/bellsoft/liberica-vm-core-23.1.2-openjdk21/LICENSE … Finally, you can set the environmental variable: $ export NIK_HOME=opt/bellsoft/liberica-vm-core-23.1.2-openjdk21 Conclusion That’s it, you are all set to work with Linux on Apple Silicon! Try experimenting with Coordinated Restore at Checkpoint accessible only to Linux users, or generate native images without any inconvenience. Happy coding! - [How to use Testcontainers with Spring Boot applications for integration testing](https://bell-sw.com/blog/how-to-use-testcontainers-with-spring-boot-applications-for-integration-testing/): Writing integration tests used to be a real time-devouring monster, especially if your application connects to a variety of external systems and you have to emulate each of them in your testing environment. Luckily, Testcontainers came to be. The solution helps to minimize the time spent on setting up a testing environment without sacrificing the reliability of test results. In this article, I’ll show you how to integrate Testcontainers into your Spring Boot projects. By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can reduce the memory footprint of your containers by up to 30 %! Table of Contents What is Testcontainers? Why you should integrate Testcontainers into your development practice Testcontainers vs mock services Using Testcontainers with Spring Boot Create a Spring Boot project Write integration tests using Testcontainers Conclusion What is Testcontainers? Testcontainers is an open-source solution that provides throwaway Docker instances of various external systems for integration testing.Testcontainers provide an extensive variety of modules: relational, noSQL, and vector databases, message brokers, clouds, etc. — basically, anything that can be put and run in a Docker container. Several languages are supported, including Java. You can use Testcontainers with applications based on popular frameworks such as Spring Boot. Why you should integrate Testcontainers into your development practice Testcontainers offer several benefits over traditional in-memory or mock services: Testcontainers minimize the time spent on setting up a testing environment because developers don’t have to write mocks to emulate all external services their application connects to. The modules offered by Testcontainers are the instances of real systems, which eliminates the risks of unexpected application behavior in the production environment even though the tests passed. Testcontainers take care of the test instances, pulling and deleting them automatically, which also contributes to accelerated CI/CD. Testcontainers vs mock services Like Testcontainers, mock services emulate the external services your application communicates with, but these two technologies are not the same. Both have advantages and disadvantages as compared to each other, as well as specific test scenarios. Mock services imitate the external systems by sending responses corresponding to user expectations. Compared to Testcontainers, there are few ready solutions for testing, so developers usually have to write their mocks, which is a complex task. In addition, mock services don’t always imitate the external system perfectly, whereas Testcontainers represent the real external systems used in production. However, once written, mock services start up immediately, so tests may run much faster than those based on Testcontainers because with Testcontainers, you have to pull a Docker image with an instance of the required external service, which, depending on the instance size and Internet speed, may take a while. Luckily though, you don’t have to restart the instance every time you recompile your app. Spring Boot, for instance, offers DevTools to introduce changes to the code on the fly without restarting the JVM, and the @RestartScope annotation for Testcontainers to keep the instance running as long as you need. But there’s another tangible benefit to mock services. They help to test all possible scenarios because they can imitate any working state including failures, which may be essential for some applications. Testcontainers also offer modules for network failures testing such as Toxiproxy Module, Microcks and WireMock for mocking APIs, but in general, they don’t offer such refined control over testing environments as tailored mocks. To conclude, Testcontainers offer a convenient way to perform integration testing without writing an emulation of an external service. They accelerate development because developers don’t waste time setting up a testing environment. On the contrary, mock services may be difficult to write, but they offer finer control over a wide range of testing scenarios. So it is possible to use Testcontainers for most tests and write mock services only when you need to test a very specific scenario. Using Testcontainers with Spring Boot You can use Testcontainers with any Java application, but I’ll show you how to integrate the solution into a Spring Boot project. Spring Boot offers first-class support for Testcontainrs including the default dependency at Spring Initializr. Prerequisites Java 21, the latest Java LTS release (I will use Liberica JDK recommended by Spring) Docker You favorite IDE Create a Spring Boot project First, let’s create a brand-new Spring Boot 3 project. Go to start.spring.io and select maven, Java 21, the latest stable Spring Boot version, and the following dependencies: Spring Web, Testcontainers, PostgreSQL Driver, Spring Data JPA, and Lombok. Creating a demo project Generate the project and open it in your IDE. Let’s create an entity class first. I’m using Lombok to reduce the boilerplate code. @Entity @Table (name = "books") @AllArgsConstructor @NoArgsConstructor @Getter @Setter public class Book { @Id @Column(name = "id", nullable = false) private Integer id; @Column(name = "title") private String title; @Column(name = "author") private String author; @Column(name = "publication_year") private int publicationYear; } Then, create a repository interface. public interface BookRepository extends JpaRepository { } Now, create a Controller class with several methods. @RestController public class BookController { private final BookRepository repository; public BookController(BookRepository repository) { this.repository = repository; } @GetMapping("/books") List findAll(){ return repository.findAll(); } @GetMapping("/books/{id}") public Book findById(@PathVariable int id) { Optional tryBook = repository.findById(id); Book book; if(tryBook.isPresent()){ book = tryBook.get(); } else{ throw new RuntimeException("Didn't find book id: " + id); } return book; } @PostMapping("/books") public Book addBook(@RequestBody Book book) { return repository.save(book); } } Create a schema.sql file in resources with the following content: create table if not exists books ( id serial primary key, title varchar(255) not null, author varchar(255) not null, publication_year int not null ); Finally, enable schema initialization in the application.properties file: spring.sql.init.mode=always Write integration tests using Testcontainers Alright, it’s time to write some integration tests for our demo Spring Boot app. Generate a Test class for our BookController. Right now, it’s absolutely empty: class BookControllerTest { } First of all, annotate the class with @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT). It helps to start the application on a random port. You will also need @Testcontainers indicating that this class uses Testcontainers: @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers class BookControllerTest { } Next, we need to tell Testcontainers to pull a Docker container image with a ready PostgreSQL instance that we’ll use for our tests. Note that there are two ways to use such containers: you can completely isolate the tests from each other by wiring up a new database instance before each test. But it may increase the time for running all tests significantly. So we will use one container for the whole test class, and I’ll show you how to manage it so that the test results don’t interfere with each other. Let’s configure the PostgreSQLContainer. Add @Container and @ServiceConnection annotations and define the name of the image. I’m using the latest PostgreSQL version, but it is recommended to set the one you use in production. @Container @ServiceConnection static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); Starting with Spring Boot 3.1, the process of setting up a container is easier because there’s no need to define the datasource properties (url, username, and password) thanks to the @ServiceConnection annotation that tells Spring Boot to configure the properties automatically. Next, define the methods to start the container before running any tests and stop it after all tests will have executed. @BeforeAll static void beforeAll() { postgresContainer.start(); } @AfterAll static void afterAll() { postgresContainer.stop(); } Add the BookRepository instance, a RestTemplate instance to perform HTTP requests, and the port that Spring Boot chose for running the application: @Autowired BookRepository repository; @LocalServerPort private Integer port; @Autowired TestRestTemplate restTemplate; As I mentioned, we will use one container for all tests meaning that we have to populate the database with some data before each test and perform some cleaning up after each test. We also need to register a base URI. So the setUp() and clear() methods will look as follows: @BeforeEach void setUp() { restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("http://localhost:" + port)); List books = List.of( new Book(1, "The Turn of the Screw", "Henry James", 1898), new Book(2, "American Gods", "Neil Gaiman", 2001), new Book(3, "Dandelion Wine", "Ray Bradbury", 1957) ); repository.saveAll(books); } @AfterEach void clear() { repository.deleteAll(); } We’re all set! Let’s create some tests. I’ll add two tests for finding all books and finding one book by id: @Test void shouldReturnBookById() { String title = "The Turn of the Screw"; ResponseEntity response = restTemplate.getForEntity("/books/1", Book.class); assertNotNull(response.getBody()); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(title, response.getBody().getTitle()); } @Test void shouldFindAllBooks() { Book[] books = restTemplate.getForObject("/books", Book[].class); assertEquals(3, books.length); } Note that with Testontainers, it is better to get as much information about the request as possible. Take a shouldReturnBookById test. If we only assert that the response body is not null, it doesn’t guarantee that the body contains the object we asked for. I deliberately broke my code so that the program couldn’t find a book by id. The test passed, but the logs contained the exception we threw in the method in case the object was null. So it would be better to check what exactly the response body contains, as well as the HTTP status of the response. Another test will verify that we can successfully create a book: @Test void shouldCreateBook() { Book book = new Book(4, "The Catcher in the Rye", "J. D. Salinger", 1951); ResponseEntity response = restTemplate.exchange("/books", HttpMethod.POST, new HttpEntity(book), Book.class); int yearExpected = 1951; assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals(yearExpected, response.getBody().getPublicationYear()); } And here’s the complete code for our BookControllerTest class: BookControllerTest class import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.util.DefaultUriBuilderFactory; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers class BookControllerTest { @Autowired BookRepository repository; @LocalServerPort private Integer port; @Autowired TestRestTemplate restTemplate; @Container @ServiceConnection static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); @BeforeAll static void beforeAll() { postgresContainer.start(); } @AfterAll static void afterAll() { postgresContainer.stop(); } @BeforeEach void setUp() { restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("http://localhost:" + port)); List books = List.of( new Book(1, "The Turn of the Screw", "Henry James", 1898), new Book(2, "American Gods", "Neil Gaiman", 2001), new Book(3, "Dandelion Wine", "Ray Bradbury", 1957) ); repository.saveAll(books); } @AfterEach void clear() { repository.deleteAll(); } @Test void shouldReturnBookById() { String title = "The Turn of the Screw"; ResponseEntity response = restTemplate.getForEntity("/books/1", Book.class); assertNotNull(response.getBody()); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(title, response.getBody().getTitle()); } @Test void shouldFindAllBooks() { Book[] books = restTemplate.getForObject("/books", Book[].class); assertEquals(3, books.length); } @Test void shouldCreateBook() { Book book = new Book(4, "The Catcher in the Rye", "J. D. Salinger", 1951); ResponseEntity response = restTemplate.exchange("/books", HttpMethod.POST, new HttpEntity(book), Book.class); int yearExpected = 1951; assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals(yearExpected, response.getBody().getPublicationYear()); } } Time for the most exciting part — let’s run it! Run the BookControllerTestClass and in a couple of seconds, you should see that all tests have passed. Running the tests Conclusion As you can see, writing integration tests with Testcontainers is extremely convenient. And Spring Boot offers integrated support of the feature, so it is definitely worth adding them to your workflow. By the way, containerizing Spring Boot apps is more convenient than ever! Try out Alpaquita Containers tailored to Spring Boot or Alpaquita Buidpack for automated containerization, and may your containers always be small and performant! Want to receive more tips and guides on Spring Boot 3 development? Subscribe to our newsletter and don't miss new articles! - [A guide to event streaming with Apache Kafka](https://bell-sw.com/blog/a-guide-to-event-streaming-with-apache-kafka/): Cloud-native development unlocks almost immense application scalability and data processing opportunities. Modern technologies can process vast amounts of various data in real-time. On the other hand, millions of users can interact with a network of services, connected to other services and databases on their end, and receive instant feedback. But the greater the prospects, the stronger the challenges. Companies nowadays can’t do without software that helps to coordinate and manage massive data flows. One of the most common solutions for these purposes is Apache Kafka. In this article, we discuss the aspects of data processing in a modern IT environment and explore the Apache Kafka architecture. In the next article, we’ll see how to install Kafka on a local machine. Table of Contents What is Apache Kafka Event handling in modern applications Event messaging Event streaming About Apache Kafka Apache Kafka architecture Kafka internals: topics, partitions, and consumer groups Conclusion What is Apache Kafka Event handling in modern applications An event is an action or occurrence usually happening asynchronously, which is recognized and can be handled by the software. Events come from the user (mouse, keyboard, touchscreen events, etc.) or the system (program or system errors, motion sensed by the sensor, messages sent from other programs, etc.) An application is called event-driven if it changes its behavior in response to the event, as opposed to a data-driven program, whose aim is to process the incoming data and output the result. Events in computing are always associated with data, so such concepts as event processing, event streaming, event messaging, etc., describe how the system handles and reacts to the incoming data. Event is an essential aspect of reactive programming, serverless computations, and microservices. Microservices are a preferable architectural approach to cloud-native development as they are more space-efficient, resilient, and scalable than monoliths. These services are loosely coupled and communicate with each other when events occur. Therefore, understanding event-handling approaches is crucial to developers working with cloud-native programs. Event messaging Event streaming and event messaging are two concepts sometimes used interchangeably, although they have several significant differences. In event messaging (or publisher-subscriber, pub-sub) design, an application responds to each message received. Each message is discrete and can be read and understood in isolation. In addition, the sender (publisher) and receiver (or subscriber) are known to each other, and the messages are sent to a specific address and get deleted upon consumption. As messages get deleted, new subscribers cannot access the message history. A commonly-used technique of message handling with this approach is message queuing. Messages sent by a publisher are stored in a queue following a FIFO (first in, first out) principle. Consumers listen to the queue for messages. The first consumer who picks up a message deals with it, and after that, the message is deleted. Each message is processed only once by a single consumer. Event streaming In event streaming, individual messages are unimportant. They are aggregated and analyzed to discover a pattern that can be acted upon. The receiver is also unknown to the sender. The sender streams messages, and interested receivers can subscribe to them. Furthermore, receivers don’t delete the messages, and new receivers have access to a history of messages and can synchronize with the current state by consuming the whole stream from the beginning. The technology that helps to analyze event streams is called “Complex Event Processing” (CEP), a set of techniques for capturing and analyzing data streams as they arrive to identify opportunities or threats in real-time. CEP enables systems and applications to respond to events, trends, and patterns in the data as they happen. Event streaming vs event messaging To sum up, these two approaches are suitable for specific purposes and can be used complementary depending on the result you want to achieve. About Apache Kafka Apache Kafka is an open-source event streaming platform initially created by LinkedIn and currently developed under the auspices of the Apache Software Foundation. It is Highly scalable (tailored to horizontal scaling), Fault-tolerant thanks to the efficient replication mechanisms, and Can be deployed on bare metal, VMs, containers, on premises, in the cloud. Kafka’s key difference from similar solutions such as RabbitMQ is that the events are not deleted right after being read by consumers. Instead, they can be stored as long as required — days, months, years, or forever — and processed multiple times, which is useful in cases of failure recoveries or to verify the code of new consumers In addition, Kafka is a powerful solution for real-time data processing and establishing communication between microservices. Apache Kafka architecture Let’s briefly discuss the structure of Kafka’s cluster and how events are managed within the system. The key components of the cluster are producers, consumers, and brokers. A producer is a client application that publishes (i.e., sends) events to a broker. An event consists of a key-value pair, a timestamp, and additional metadata. A broker (also known as a Kafka server or a Kafka node) is responsible for storing the data, and it acts as a bridge between producers that publish events and consumers reading these events. Upon receiving an event from a producer, a broker stores it in a dedicated topic. Each Kafka broker can host several topics, and each producer and consumer can write/read events to several brokers. A consumer is a client application that reads events from the topic it is subscribed to using the pull model meaning that it sends requests to the server to receive a new batch of events. Apache Kafka cluster architecture But what if a broker fails? Or a consumer? How exactly is Fafka’s fault-tolerance guaranteed? And besides, if the events can be stored for a prolonged period of time, how do we know that the consumer doesn’t read the same events over and over again? To answer these questions, we have to dive deeper into the system structure. Kafka internals: topics, partitions, and consumer groups The events in Kafka are not stored in the topics randomly. Each topic is divided into partitions representing replicable logs stored on the disc. The events are always written to the partition head according to their keys guaranteeing the read/write order. Besides, each event receives a unique offset number, so in case of restart, a consumer can continue from the offset it registered. The partitions are always replicated n times among multiple brokers. The replication factor can be adjusted: for instance, if it equals 3, there are three copies of a partition across various brokers. Therefore, even if a broker fails, consumers can continue reading events from relevant partitions from other brokers. The consumers do not stand alone — they belong to consumer groups. A consumer group can have one consumer or several. Ideally, the number of consumers equals the number of partitions in a topic, so that the load can be evenly distributed among them. But if one consumer fails, the load is automatically redistributed among the remaining consumers. On the other hand, if the consumers can’t keep up with the load, you can add an additional consumer to the group and at the same time, one more partition to the topic. Only then can a new consumer get involved into the event processing. Apache Kafka partitions and consumer groups It is important to note that right now, Kafka relies on ZooKeeper, an open-source server by the Apache Foundation, to store metadata about partitions and brokers. ZooKeeper is set up independently of Kafka nodes and represents a distributed data storage system that has to be controlled separately, adding complexity to the platform management. What is worse, if the ZooKeeper fails, the data on current processes within the cluster will be lost, making it extremely challenging to restore the workflow. This is one of the reasons why the Kafka team decided to substitute ZooKeeper with a more robust modern solution called KRaft. In short, KRaft allows for storing metadata as topics within the Kafka cluster, making metadata processing faster and more scalable. In addition, metadata topics are replicated among brokers, which increases fault tolerance. You can read more about KRaft in the dedicated Kafka Improvement Proposal KIP-500. Zookeeper is officially deprecated in Kafka 3.5 and is set for removal in version 4.0. But migration to KRaft is still in early stages as the feature is not yet-production ready. Conclusion Apache Kafka is a complex system, and understanding its key concepts is essential for setting up effective communication between the services. In the next article, we’ll dive into setting up Kafka on a local machine, and even more tutorials are on the way, so subscribe to our newsletter and don’t miss them! - [BellSoft releases Alpaquita Containers with Coordinated Restore at Checkpoint support](https://bell-sw.com/blog/bellsoft-releases-alpaquita-containers-with-coordinated-restore-at-checkpoint-support/): We are happy to announce the release of Alpaquita Containers with Alpaquita Linux, Liberica JDK, and support for the OpenJDK Coordinated Restore at Checkpoint (CRaC) API. Ready-to-use container images will enable the developers to seamlessly integrate CRaC into their projects. The solution is available for JDK 17 and 21 and x86_64 architecture, with ARM support coming later in 2024. Get Alpaquita Containers with CRaC Table of Contents Smaller instances and higher availability with CRaC Alpaquita Containers with CRaC provide up to 164x faster startup and 1.1x smaller images “Plug-and-play” CRaC experience with Alpaquita Containers Download Alpaquita Containers with CRaC support now! Smaller instances and higher availability with CRaC Typical enterprise Java applications take several minutes to reach stable peak performance (i.e., to warm up). In addition, they tend to consume more resources during warmup than they actually need for stable operation. As a result, You overpay for cloud resources by allocating more memory to your instances than they actually need. Cloud expenses increase even more because you pay for CPU cycles during warmup. During the warmup, instances don’t process client requests, increasing latency and user dissatisfaction. Coordinated Restore at Checkpoint is an OpenJDK API aimed at solving these issues by providing the developers with the ability to start up their instances at peak performance. Find out more about the aspects of the CRaC API in our What is CRaC? article. Alpaquita Containers with CRaC provide up to 164x faster startup and 1.1x smaller images We measured the performance of Alpaquita Containers with CRaC support using the Spring Boot Petclinic application, and the results for startup and footprint reduction are quite impressive! Experimental setup: CPU: Intel(R) Xeon(R) CPU X5675 @ 3.07GHz, JDK: Liberica JDK 21, OS: Alpaquita Linux with glibc, GC implementation and heap size: G1 GC (-Xms512m -Xmx512m). In terms of startup, the tests yielded the reduction of startup (i.e., time to first operation) from 4935 ms to 30 ms. Spring Boot Petclinic and Alpaquita Containers with CRaC: startup study results Similar results are observed with ParallelGC and ShenandoahGC. As far as the CRaC image reduction is concerned, we measured Resident Set Size (RSS) before the checkpoint, after restore, and after a number of requests. We can see that the RSS before the dump is larger than the RSS after the checkpoint. After the first request, the RSS increases and reaches some steady state. However, it is still less than the original RSS before the checkpoint. The RSS reduction is achieved thanks to the following factors: Liberica JDK with CRaC performs Full Garbage Collection before the checkpoint, freeing some pages. While criu, the executable used by Liberica JDK with CRaC for dumping, is running, some pages belonging to the Java Virtual Machine or used by system libraries may not be used after recovery. These memory pages can still be used by the libraries or JVM, but they are only reserved for future use after restoring. HotSpot VM returns part of native memory to the OS (including pages that were freed during GC). As a result, the size of the generated CRaC image in our case was reduced from 613 MB to 190 MB right after restore. When the application started processing requests, the image size grew to 381 MB after 10,000 requests. The image size didn’t change afterwards. So the general reduction by 1.1x times is still tangible. Spring Boot Petclinic and Alpaquita Containers with CRaC: image footprint study results Note that the RSS reduction is load-specific and may be different in case of other applications. Therefore, implementing Alpaquita Containers with CRaC to your Java workloads will enable you to Create instances fitting your application perfectly and not overpay for resources that you don’t use. Minimize latency and process more client requests from the start. Scale efficiently by adding the exact amount of instances and memory your application needs to manage the increased load. “Plug-and-play” CRaC experience with Alpaquita Containers BellSoft has already released Liberica JDK builds with CRaC API support in November 2023. But why is it important to have a ready container image with CRaC support? The CRaC API is not pure OpenJDK code, it relies on the CRIU (Checkpoint and Restore in Userspace) technology found in Linux-based operating systems. BellSoft developed Alpaquita Containers so that developers can use CRaC with their projects without additionally adjusting Java or the OS. Alpaquita Containers include Liberica JDK 17 or 21, an OpenJDK distribution recommended by Spring, with CRaC API support, and Alpaquita Linux, a lightweight Alpine-inspired distribution offering two flavors, with optimized musl or glibc. Download Alpaquita Containers with CRaC support now! Alpaquita Containers are free to use. BellSoft’s Docker Hub repository contains a variety of images with CRaC support, so you can choose the perfect one for your workloads. Documentation on Alpaquita Containers contains detailed information on choosing, pulling, and using images. To get started with the feature, read our tutorial on using CRaC with Spring Boot or consult our engineers, and we’ll be happy to help! Get Alpaquita Containers with CRaC - [How to use CRaC with Spring Boot apps in a Docker container](https://bell-sw.com/blog/how-to-use-crac-with-spring-boot-apps-in-a-docker-container/): Alpaquita Containers with Alpaquita Linux, Liberica JDK, and support for the Coordinated Restore at Checkpoint (CRaC) offer the developers the “plug-and-play” experience with the OpenJDK CRaC API. Without the need to adjust the JDK or OS, they can conveniently perform the checkpoint and restore the warmed up state of their containerized Java workloads. This article will guide you through using CRaC with Spring Boot applications running in a Docker container. Table of Contents Prerequisites Prepare a Spring Boot app with CRaC Prepare a Dockerfile to checkpoint the application in a container Containerize and start the application in a Docker container Transfer the checkpointed app to a new container Conclusion Prerequisites Alpaquita Containers with CRaC support (You can download containers with CRaC on our Docker Hub repository under the ‘crac’ tag) Spring Boot 3.2+ Docker Prepare a Spring Boot app with CRaC For our tutorial, we will use a Spring Boot reference application, Spring Petclinic. First of all, clone the Spring Petclinic's repository: git clone https://github.com/spring-projects/spring-petclinic.git To use CRaC with the new Spring Boot and Spring Framework, we need to add the dependency for the org.crac/crac package to pom.xml: org.crac crac 1.4.0 Let's check the difference of our updated pom.xml: git diff diff --git a/pom.xml b/pom.xml index 287a08a..f403155 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,11 @@ + + org.crac + crac + 1.4.0 + org.springframework.boot Prepare a Dockerfile to checkpoint the application in a container CRaC enables the developers to save the exact state of a running Java application (together with the information about the heap, JIT-compiled code, and so on). We will use Docker multi-stage builds functionality to containerize the application and checkpoint the running app in a container. Copy the following Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-21-crac-musl as builder WORKDIR /home/app ADD spring-petclinic-main /home/app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package FROM bellsoft/liberica-runtime-container:jre-21-crac-slim-musl as optimizer WORKDIR /app COPY --from=builder /home/app/spring-petclinic-main/target/spring-petclinic-3.3.0-SNAPSHOT.jar /app/app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --launcher FROM bellsoft/liberica-runtime-container:jre-21-crac-slim-musl # We stay root in a container to use CRaC VOLUME /tmp EXPOSE 8080 COPY --from=optimizer /app/app/dependencies/ ./ COPY --from=optimizer /app/app/spring-boot-loader/ ./ COPY --from=optimizer /app/app/snapshot-dependencies/ ./ COPY --from=optimizer /app/app/application/ ./ ENTRYPOINT ["java", "-Dspring.context.checkpoint=onRefresh", "-XX:CRaCCheckpointTo=/checkpoint", "-XX:MaxRAMPercentage=80.0", "org.springframework.boot.loader.launch.JarLauncher"] At the first stage, we build the application. At the second stage, we make use of Spring Boot layered jars. This functionality enables us to store application classes and dependencies in different layers. Note that we need a special JarLauncher class later to work with layered images. At the third stage, we run the application and perform the automatic checkpoint using Spring’s -Dspring.context.checkpoint=onRefresh option to start and immediately exit the application after non-lazy beans have been instantiated and InitializingBean#afterPropertiesSet callbacks have been invoked. Containerize and start the application in a Docker container We use the bellsoft/liberica-runtime-container:jdk-21-crac-musl image to start Petclinic and get the application dump for further restore. Note that BellSoft also provides images with glibc libc. Another important prerequisite is the kernel version of the underlying Linux distribution, which should be at least 5.9. Linux kernel 5.9 introduces the CAP_CHECKPOINT_RESTORE option, which separates the checkpoint/restore functionality from the CAP_SYS_ADMIN option and enables the developers to steer clear of running containers with elevated permissions. The CAP_CHECKPOINT_RESTORE (and SYS_PTRACE, which is also required for checkpoint-restore) options are enabled with the --cap-add flag added to newer Docker versions, so make sure you have the latest available Docker version. Use the following command to build a Docker container: $ docker build . -t petclinic-advanced-crac-checkpoint -f Dockerfile-advanced-crac Now, let’s run the application in a container. If you use a Linux distribution with an older kernel version, you can use the --priviledged flag instead of CAP_CHECKPOINT_RESTORE and SYS_PTRACE (however, this is not the best practice to run containers with elevated permissions). docker run --cap-add CHECKPOINT_RESTORE --cap-add SYS_PTRACE --name petclinic-crac petclinic-advanced-crac-checkpoint Note that there’s no --rm option as we'll remove the container manually later. After that, the application will be automatically stopped and checkpointed. Transfer the checkpointed app to a new container Let’s create a fresh image with the checkpointed application: $ docker commit --change='ENTRYPOINT ["java", "-XX:CRaCRestoreFrom=/checkpoint"]' petclinic-crac petclinic-advanced-crac-restore Now, remove the previous container manually: $ docker rm -f petclinic-crac You can now start the application by restoring it: $ docker run -it --rm -p 8080:8080 --cap-add CHECKPOINT_RESTORE --cap-add SYS_PTRACE --name petclinic-crac petclinic-advanced-crac-restore Please note that the -p 8080:8080 option is added to expose the port for the running application. It is also possible to start other Petclinic instances in separate containers — just use another port on the host system and another container name. To verify that the app is working correctly, open http://localhost:8080. If there are other instances started, you can open Spring Petclinic using a different port, for example, http://localhost:8081 if the port is opened as in the previous command. Conclusion As you can see, CRaC API can be conveniently used with your containerized workloads thanks to ready-to-use Alpaquita Containers. If you have any questions regarding this feature, feel free to reach out, and our engineers will be happy to help! Contact Us - [BellSoft releases Alpaquita Containers with Coordinated Restore at Checkpoint support](https://bell-sw.com/news/bellsoft-releases-alpaquita-containers-with-coordinated-restore-at-checkpoint-support/): SAN JOSE, Calif., March 14, 2024 /PRNewswire/ -- BellSoft, the OpenJDK vendor that delivers the most complete Java experience, is excited to announce the release of Alpaquita containers with Coordinated Restore at Checkpoint (CRaC) support, rounding up your Java journey with an end-to-end CRaC solution. Java adaptation to the cloud environment comes with challenges, and the so-called "slow Java startup and warmup" is the one well-known. It can be described as a process that takes both time and memory for the traditional JVM to reach its exceptional peak performance in modern applications, resulting in higher costs and slower performance. The Java community was looking for a solution to solve all the issues of slow startup at once: to decrease time-to-performance, resources-to-performance and startup cost. The Coordinated Restore at Checkpoint (CRaC) Project is the most current promising answer. "Plug-and-play" CRaC experience with Alpaquita Containers BellSoft's Alpaquita containers are available for JDK 17 and 21 and x86_64 architecture, with ARM support coming later in 2024. What is inside of them? Liberica JDK 17 or 21, an OpenJDK distribution recommended by Spring, with CRaC API support, Alpaquita Linux, a lightweight Alpine-inspired distribution offering musl or glibc, with an OpenJDK CRaC package. "Alpaquita containers are made for Java applications in the cloud, strengthened with CRaC support; they help mitigate the problem of slow startup and warmup," said Alex Belokrlykov, BellSoft's CEO. "Use Alpaquita containers with CRaC for your Spring application for an outstanding performance. The Alpaquita container with CRaC support adds to the group of BellSoft's products to make your Java journey complete, smooth, sustainable, and cloud-native". "Liberica JDK provides a crucial value for the community, providing a one-stop shop for a modern, tested OpenJDK distribution that supports GraalVM native images, or CRaC capable instant-on application workloads." - Josh Long, Spring Developer Advocate. Get your Alpaquita container with CRaR here to benefit your Java development immediately. About BellSoft BellSoft delivers the most complete Java experience with a more secure, reliable, and cost-effective approach to application development on any platform and in any environment. BellSoft is one of the leading contributors to the OpenJDK, and the only vendor that supports current LTS Java versions, legacy JDK 6 & 7 and Liberica NIK. Liberica JDK is the runtime of choice for VMware, Spring Framework, JetBrains, and millions of users worldwide. For more information, visit www.bell-sw.com. - [Liberica JDK 22 is released](https://bell-sw.com/blog/liberica-jdk-22-is-released/): We are happy to announce the general availability of Liberica JDK 22 builds! The new version is packed with fixes and new and improved functionality: 313 fixes overall — 175 in JDK and 138 in FX. BellSoft engineers resolved 6 issues. 12 JEPs with new or improved features. Download Liberica JDK 22 Summary of integrated JEPs Despite the fact that JDK 22 is a non-LTS release, the platform doesn’t stay still and is evolving constantly. Key improvements in this version are aimed at Enhancing performance with JEP 423: Region Pinning for G1 JEP 460: Vector API (Seventh Incubator) Increasing reliability with JEP 454: Foreign Function & Memory API JEP 457: Class-File API (Preview) JEP 462: Structured Concurrency (Second Preview) JEP 464: Scoped Values (Second Preview) Facilitating development with JEP 447: Statements before super(...) (Preview) JEP 456: Unnamed Variables & Patterns JEP 458: Launch Multi-File Source-Code Programs JEP 459: String Templates (Second Preview) JEP 461: Stream Gatherers (Preview) JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) You can read more about all 12 JEPs in our article dedicated to the topic. Download the new JDK 22 builds now! Whether you are planning the migration to the next LTS release or just want to experiment with new Java features, you can download and use Liberica JDK for free. Head over to Liberica JDK Download Center to get the fresh builds now! Download Liberica JDK 22 - [Liberica Native Image Kit 24.0.0 build is released](https://bell-sw.com/blog/liberica-native-image-kit-24-0-0-build-is-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 24.0.0 for JDK 22. The builds contain several new features. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. New distribution method of Liberica NIK 24 Starting with Liberica NIK 24.0.0 and up, the distribution method is different from the previous versions. Before, you would download the Liberica NIK Standard package and then install plugins for programming languages other than Java. To compile Java applications you would utilize Core and Full packages (with or without JavaFX). With the release of Liberica NIK 24 we introduced several changes: Core Package is gone, you should use the Standard package instead. Standard Package and Full Package are used for Java applications only (with or without JavaFX). To build native images for applications written in other programming languages, you now download the stand-alone Package of Liberica NIK dedicated to the language of your choosing. Every non-Java Package can be downloaded as Java- or Native-based build. We provided a tutorial on choosing the correct Package of Liberica NIK 24 on the Download page. Notable improvements Better support for AWT and JavaFX fullscreen mode. Intrinsified memory copying routines on AMD64 platforms. Where available, they now use AVX instructions for better performance. Improved SubstrateVM monitor enter/exit routines for accelerated startup of native images. Head to this article for more details on the improvement. Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [How to install Apache Kafka on a local machine](https://bell-sw.com/blog/how-to-install-apache-kafka-on-a-local-machine/): Our previous article explained how Apache Kafka provides an efficient and resilient platform for event streaming. It’s time for some coding! In this article, we will set up Kafka on our local machine, which is useful when you want to explore the technology without disrupting the production environment. By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can reduce the memory footprint of your containers by up to 30%! Ad recently they became even more powerful with added CRaC support that enables to reduce the startup and warmup time of your services for minutes to milliseconds. Follow the guide on using CRaC with Java in a container and start experimenting with the feature. Table of Contents Install Java Download and set up Kafka with KRaft Create a Kafka topic Write and read events to and from the topic Adjust the event retention policies Stop the Kafka cluster Conclusion Install Java To work with Kafka, you need to install Java. Kafra requires JDK 8 or later. I will use JDK 21, the latest LTS release, and recommend you to try it out as it is packed with new and enhanced features facilitating development and increasing JVM performance. Note that the commands below are provided for Linux and macOS, so if you use Windows, you can follow this guide by installing WSL to create a Linux environment for your Kafka cluster. Download Java 21 for your platform. You can also pull the bundle from Linux repositories or via package managers. Make sure that the JAVA_HOME variable is set to Liberica JDK installation directory: $ export JAVA_HOME= Verify the installation by running $ java -version The output will be similar to openjdk version "21.0.2" 2024-01-16 LTS OpenJDK Runtime Environment (build 21.0.2+14-LTS) OpenJDK 64-Bit Server VM (build 21.0.2+14-LTS, mixed mode, sharing) Download and set up Kafka with KRaft Download the latest stable release of Apache Kafka (I got 3.6.1, your version may be different) Note that you need to download the binary file, not the source! Open the Terminal, unzip the file and go to the Kafka root directory. $ tar -xzf kafka_2.13-3.6.1.tgz $ cd kafka_2.13-3.6.1 Alternatively, you can install Kafka via a package manager, for instance, Homebrew: $ brew install kafka To set up a Kafka cluster, you need Zookeeper or KRaft. I suggest we use KRaft as Zookeeper is deprecated and will soon be removed. First of all, we need to generate the cluster ID. Run $ bin/kafka-storage.sh random-uuid This will yield a UUID, which you need to add to the KRaft server.properties file. You can format the file from the command line ( replace the with the UUID generated at the previous step) : $ bin/kafka-storage.sh format -t -c config/kraft/server.properties Formatting /tmp/kraft-combined-logs with metadata.version 3.6-IV2. You can now launch the Kafka Server with KRaft with the following command: $ bin/kafka-server-start.sh config/kraft/server.properties … [2024-02-16 12:50:49,056] INFO [BrokerServer id=1] Transition from STARTING to STARTED (kafka.server.BrokerServer) [2024-02-16 12:50:49,057] INFO Kafka version: 3.6.1 (org.apache.kafka.common.utils.AppInfoParser) [2024-02-16 12:50:49,057] INFO Kafka commitId: 5e3c2b738d253ff5 (org.apache.kafka.common.utils.AppInfoParser) [2024-02-16 12:50:49,057] INFO Kafka startTimeMs: 1708077049056 (org.apache.kafka.common.utils.AppInfoParser) [2024-02-16 12:50:49,061] INFO [KafkaRaftServer nodeId=1] Kafka Server started (kafka.server.KafkaRaftServer) Closing this window will stop the cluster, so open a new Terminal window, and let’s proceed with creating our first topic. Create a Kafka topic To read, write, or process events with Kafka, we first need to create a topic where these events will be stored. For that purpose, we will use the kafka-topics.sh script in the bin directory and pass several options to it: --create to create a new topic, --topic with the topic name, --bootstrap-server to connect the script to the server. Right now, we have only one broker, so we will specify localhost and the default port Kafka listens to, 9092 The full command looks like this: $ bin/kafka-topics.sh --create --topic user-requests --bootstrap-server localhost:9092 Created topic user-requests. You can see the detailed information about your topic by running $ bin/kafka-topics.sh --describe --topic user-requests --bootstrap-server localhost:9092 Topic: user-requests TopicId: pMx2tM65SzOYUWMD02IjoQ PartitionCount: 1 ReplicationFactor: 1 Configs: segment.bytes=1073741824 Topic: user-requests Partition: 0 Leader: 1 Replicas: 1 Isr: 1 As we didn’t specify the number of partitions, the resulting topic has one partition with ID=0 and a leader with ID=1. Write and read events to and from the topic To write events to our topic, we need to start a producer. We can do that by running the kafka-console-producer.sh script in the bin directory and specifying the name of the topic and the bootstrap-server: $ bin/kafka-console-producer.sh --topic user-requests --bootstrap-server localhost:9092 > Each line you write will be written to the topic as a separate event. For instance, >User A left a request >User B left a request >User C left a request After having written several messages, you can stop the producer with Ctrl-C. Okay, we have some events, now we want to read them. For that purpose, we need to start a consumer with the kafka-console-consumer.sh script: $ bin/kafka-console-consumer.sh --topic user-requests --from-beginning --bootstrap-server localhost:9092 User A left a request User B left a request User C left a request Note that if you run the command without the --from-beginning flag, there will be no output to the console because by default, consumers start reading events from the head of the partition in the topic from the moment they started. To change this behavior, you can adjust the offset with the --consumer-property instead of using the --from-beginning flag: $ bin/kafka-console-consumer.sh --topic user-requests --bootstrap-server localhost:9092 --consumer-property auto.offset.reset=earliest You can verify that the consumer reads the data in real-time. Open a new Terminal window, start the consumer, and in the previous window, start the producer. You will see that the events you send via the producer are immediately read by the consumer. Adjust the event retention policies You can store the events for as long as you need: a minute, a day, a month, etc. We didn’t change the default retention policy, so our events will be stored forever. If you run $ ls -la /tmp/kraft-combined-logs/user-requests-0 You will notice that there’s one .log file. Open it and you’ll see that our events are saved there. The fastest way to delete old events would be to set the retention.ms configuration for our topic, say, for one minute. But it works only if you stop the producer and wait for a minute. What if the producer writes events continuously? The thing is, events are stored in partitions in segments. Once the segment fills in, it closes and another one opens. The closed segment can be deleted. If you open the server.properties file in the config directory, you will see the following lines: # The minimum age of a log file to be eligible for deletion due to age log.retention.hours=168 # The maximum size of a log segment file. When this size is reached a new log segment will be created. log.segment.bytes=1073741824 # The interval at which log segments are checked to see if they can be deleted according # to the retention policies log.retention.check.interval.ms=300000 It means that by default, the segment (or a log file) closes in one week or after reaching 1GB in size. The log.retention.check.interval property indicates that the segments’ condition is checked every 5 minutes. You can change the settings at the broker level and at the topic level so that you have global settings for all topics and custom settings for specific ones. Let’s change the settings at the broker level. Stop the broker and change the log.retention.hours to log.retention.ms=1000 so that the segments are closed every second, and set log.retention.check.interval.ms=10000 so that the log segments are checked every 10 seconds. Restart the broker. Start the consumer with the --from-beginning property and the output will be empty (and if you check the .log file in /temp, you’ll see it is empty, too). Let’s play around. Start the producer in a separate window and write several events, one by one. If you run ls -la /tmp/kraft-combined-logs/user-requests-0 in less than 10 seconds, you will see additional .log.deleted files — these are our closed segments. But if you run this command in 10 seconds after you sent your last message, these files will be gone. You can rerun the consumer and see that the output is indeed empty. Stop the Kafka cluster After you’re done with experiments, terminate the consumer and producer with Ctrl-C, and then stop the broker with Ctrl-C. Conclusion Congratulations, you set up your first Kafka cluster! But this is just the tip of the iceberg. In the next article, we will learn how to use Apache Kafka with Spring Boot microservices. Subscribe to our newsletter so as not to miss the new guides and tutorials! - [CVE-2024-3094: a backdoor in XZ Utils](https://bell-sw.com/blog/cve-2024-3094-a-backdoor-in-xz-utils/): On March 29th, 2024, malicious code was discovered in xz-utils, which is a popular open-source library for lossless data compression. This library is used in most Linux distributions. Alpaquita Linux is unaffected by this backdoor, but we downgraded the xz package out of precaution and included a patch. We recommend updating your Alpaquita image immediately as it is currently unknown whether the vulnerability is associated with other backdoors. Find out more about the CVE and how to update the library below. Table of Contents Description Risk scope Mitigation Description The backdoor is injected into the xz package at build stage. The versions 5.6.0 and 5.6.1 are affected. If the running program has the process name /usr/sbin/sshd, the payload is activated (other possible activation scenarios are currently under investigation). The payload uses the IFUNC mechanism and reads the message sent by an attacker. If this message is signed with a specific private key, the payload executes its contents on the vulnerable system. The liblzma package turns out to be in sshd in Linux distributions that use systemd because they have a linked libsystemd with liblzma. The person who injected malicious code into xz-utils has been preparing their attack for several years, building a reputation in the OSS community, gaining trust and eventually, the access to the xz project as an additional maintainer. The attacker or the group of attackers also pushed for linking sshd with libsystemd in several Linux distros to make sshd load liblzma (this didn't get into stable builds though). Luckily, the vulnerability was quickly discovered by the OSS community. The repository was banned and the account of the attacker was suspended. Other projects in which the attacker took part are currently being analyzed as well. Risk scope The backdoor targets sshd binaries linked with libsystemd and glibc, so your system is definitely affected under the following circumstances: A glibc-based Linux distribution with systemd is used (it is currently unknown whether musl-based distros are affected), The installed version of xz is 5.6.0 or 5.6.1 (these versions are used in rolling releases, no stable versions are affected), A publicly accessible OpenSSH server process is running. The vulnerability is considered critical because it allows remote root access to an authenticated attacker. Note that this is the only scenario definitely known for now but the investigation is ongoing so other backdoors may be discovered later. Neither glibc- nor musl-based Alpaquita uses systemd (which has a dependency on xz/liblzma), which means sshd is not linked to xz/liblzma via systemd and hence Alpaquita is not affected by this CVE. Mitigation Alpaquita Linux is not affected by this backdoor. However, as it is unknown whether there are other backdoors associated with this vulnerability, out of caution, we downgraded the xz version to 5.2.5 (prior to any commits from the attacker) and also included CVE-2022-1271 fix in this version. To ensure that the version of xz packages installed is the latest without any commits from the attacker, run: apk update && apk upgrade xz* && apk version xz* Verify that the version is 5.6.1_p525 (Alpaquita stream) and 5.2.9_p525 (Alpaquita 23-LTS). Here, p525 means that the actual version of the package is 5.2.5. If you use alpaquita-linux-python or alpaquita-linux-gcc, you should immediately update the image to receive the patch. Other BellSoft’s public container images based on Alpaquita Stream, don’t contain the library; however, if you do use the xz library in the containers, we recommend you to rebuild the image to include the updated version of the library. - [GitHub Actions tutorial](https://bell-sw.com/blog/github-actions-tutorial/): GitHub Actions is a popular solution for automating CI/CD. If you are getting started with GitHub Actions, in this article, we will guide you through the core concepts of the platform and provide a step-by-step guide to setting Liberica Native Image Kit with a special Github Action, which will serve as an example of integrating the platform into your every-day life. Table of Contents What is GitHub Actions: key concepts How to use GitHub Actions GitHub Action for GraalVM How to set up Liberica Native Image Kit with a GitHub Action GitHub Actions Summary What is GitHub Actions: key concepts GitHub Actions is a platform that allows for build, test, and deployment automation within a GitHub repository. The core concept of GitHub Actions is a workflow, which is a set of one or more jobs triggered when a specific event in a repository occurs (for example, a pull request or a commit). Each job is executed within its own virtual machine called runner and is divided into several steps. A step executes a defined command, script, or an action, which is a standalone reusable set of commands (it is also called extension). Actions perform complex tasks and help developers to minimize repetitive work. You can create your own action or browse the GitHub Marketplace for a ready one. Find more information about utilizing actions in the How to use GitHub Actions section below. Workflows are defined in a YAML file. This file includes the description of the trigger events, scripts that need to be run, the environment, and any other necessary information.The YAML files describing workflows are stored in the .github/workflows directory in the project. GitHub Actions is not the only automation platform. But there are features distinguishing it from other CI/CD tools. You can read more about them in our previous article. How to use GitHub Actions You can specify your own workflow by creating a .github/workflows repository in your project and putting there a file with the .yml or .yaml extension (let’s name it demo.yml, for example) with a similar content: name: Java Tests on: pull_request: jobs: build: uses: some-custom-scriptl@v1 Here, you define the name of the workflow, the trigger event (on: pull_request) and a job that executes the exact version of your script, in this case, version 1 (uses: some-custom-scriptl@v1). When you commit and push these changes, they will be automatically installed in your GutHub repository and run every time you create a pull request. However, as we already mentioned, you can browse a wide selection of ready actions and integrate the ones you require into your project. For instance, there’s a Setup Java action to download and install a required version of a Java runtime (you can choose Liberica JDK as a distribution). There is now also an action for setting up GraalVM (including Liberica Native Image Kit), which we discuss in more details below. GitHub Action for GraalVM GraalVM is a Java Virtual Machine and a Java Development Kit written in Java. It aims to increase the performance of JVM-based applications and enables the developers to introduce various programming languages to their project. One powerful feature of GraalVM is Native Image technology that converts Java applications into fully-compiled native executables Ahead-of-time, guaranteeing almost instant startup. To learn more about the platform, refer to our previous article A guide to GraalVM. A GutHub Action for GraalVM enables you to install various distributions of GraalVM: Oracle GraalVM, GraalVM Community Edition (CE), Enterprise Edition (EE), Mandrel, Liberica Native Image Kit, As well as necessary GraalVM components: Native Image, GraalVM components (e.g., Truffle languages). The action performs the following steps among others: it downloads required packages, exports a $GRAALVM_HOME environment variable, and adds $GRAALVM_HOME/bin to $PATH. How to set up Liberica Native Image Kit with a GitHub Action Liberica Native Image Kit (NIK) is a GraalVM CE-based Native Image compiler used by default in Spring Boot buildpacks for generating native images. As opposed to GraalVM CE that offers only SerialGC, Liberica NIK integrates ParallelGC allowing for significant latency reduction. Liberica NIK also supports JavaFX for converting desktop applications into native executables. Starting with version 1.2.0, the setup-graalvm action supports Liberica Native Image Kit. Below is the YAML file for using it: name: Native AOT build on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1.2 with: distribution: 'liberica' java-version: '21' java-package: 'jdk+fx' - name: Show paths run: | echo "GRAALVM_HOME: $GRAALVM_HOME" echo "JAVA_HOME: $JAVA_HOME" java --version native-image --version - name: Build native image run: native-image -jar ... Let’s look more closely at the configuration: distribution: Use 'liberica' for Liberica NIK java-version: Use the exact JDK version number such as '21.0.2' or just '21' java-package: Use 'jdk+fx' to build a JavaFX based app. Otherwise, you can simply omit this input or use 'jdk', which is the default package. The action described in the file above sets up JAVA_HOME and GRAALVM_HOME environment variables: $ echo "GRAALVM_HOME: $GRAALVM_HOME" GRAALVM_HOME: /opt/hostedtoolcache/bellsoft-liberica-vm-linux-amd64/21.0.2/x64/bellsoft-liberica-vm-openjdk21-23.1.2 $ echo "JAVA_HOME: $JAVA_HOME" JAVA_HOME: /opt/hostedtoolcache/bellsoft-liberica-vm-linux-amd64/21.0.2/x64/bellsoft-liberica-vm-openjdk21-23.1.2 It also adds them to the PATH environment variable: $ java --version openjdk 21.0.2 2024-01-16 LTS OpenJDK Runtime Environment Liberica-NIK-23.1.2-1 (build 21.0.2+14-LTS) OpenJDK 64-Bit Server VM Liberica-NIK-23.1.2-1 (build 21.0.2+14-LTS, mixed mode, sharing) $ native-image --version native-image 21.0.2 2024-01-16 GraalVM Runtime Environment Liberica-NIK-23.1.2-1 (build 21.0.2+14-LTS) Substrate VM Liberica-NIK-23.1.2-1 (build 21.0.2+14-LTS, serial gc) Now you can proceed with building a native image for your app: $ native-image -jar app.jar ... GitHub Actions Summary To sum up, GitHub Actions makes working with GtHub even more convenient. You can write your own workflows or implement the existing actions that describe complex tasks, thus eliminating the necessity of writing a lot of additional code and accelerating CI/CD. - [What is CRaC? A guide to cutting Java startup and warmup from minutes to milliseconds](https://bell-sw.com/blog/what-is-crac-a-guide-to-cutting-java-startup-and-warmup-from-minutes-to-milliseconds/): In our previous article, we discussed the issues of Java startup. Java services may need dozens of seconds to reach peak performance, and during this period, they process fewer requests and consume more memory. And the process begins from ground zero every time you start your services! There are several solutions to the problem: Application Class Data Sharing (AppCDS) helps to reduce startup by creating an archive of pre-initialized JVM and application classes so that the application doesn’t initialize the classes from scratch, but takes ready metadata from the dump. GraalVM Native Image uses ahead-of-time compilation and classes initialization to create a single native image with an almost instant startup but may not be suitable for some projects if they rely heavily on the dynamic Java features. Project Leyden uses AppCDS to load class metadata and compiled code during the training runs to reduce Java startup and warmup during the production run. The project is still in the makings, but early-access builds are available. Coordinated Restore at Checkpoint is an OpenJDK Project that helps to minimize startup and warmup and preserve the benefits of JIT-compilation for further performance optimization. As BellSoft recently released Liberica JDK 17 & 21 builds and ready-to-use containers with CRaC support so that you can already download Java with CRaC support, we would like to shed light on this exciting feature and how it will benefit your Java project. Table of Contents What is Coordinated Restore at Checkpoint (CRaC)? Coordinated processes for enhanced reliability CRIU vs CRaC Spring Boot with CRaC Which applications need CRaC Main considerations when using CRaC Download JDK builds with CRaC and start experimenting! What is Coordinated Restore at Checkpoint (CRaC)? CRaC offers Java developers a Checkpoint/Restore API to create an image of a running application at an arbitrary point in time (“checkpoint”), and then start the image from the checkpoint file (snapshot), restoring the application’s state from the moment when the checkpoint was made. Essentially, Java with CRaC enables you to pause the application and restart it from the moment it was paused. Additionally, you can distribute numerous replicas of this file, which is especially relevant for deployment on multiple instances. Coordinated processes for accident-free checkpoint Coordinated Checkpoint/Restore makes the application aware that it is being paused and restarted. This way, the application can cancel the checkpoint if it deems the moment unsuitable for saving the state (when it performs certain operations such as saving the user data, for example). In addition, it can perform essential preliminary tasks, such as closing network connections and open file descriptors, and then return to normal operation after restore and react to possible changes in the environment since the checkpoint. CRIU vs CRaC CRIU (Checkpoint and Restore in Userspace) is a technology for Linux that serves as a foundation for CRaC. CRIU uses the ptrace kernel interface and allows freezing a running application and restoring it from the saved checkpoint files. The existing OpenJDK CRaC implementation includes CRIU and adds several enhancements and adjustments tailored to Java applications. Namely, CRaC imposes more restrictions on the restore process. For instance, CRIU can save the state of a TCP socket and then restore the connection. With CRaC, all connections must be closed before checkpoint, which makes the whole process more reliable. Spring Boot with CRaC Spring Boot currently integrates with CRaC as a Proof-of-Concept. Thanks to CRaC support, Liberica JDK, the recommended runtime for Spring, will help developers in smoothly integrating the functionality into their Spring Boot projects enabling unprecedented startup and warmup speed with minimal code rewriting. Moreover, Alpaquita Containers with CRaC offer “plug-and-play” experience with CRaC API, allowing seamless integration into your workloads and achieving up to 164 times faster startup! Spring Boot Petclinic and Alpaquita Containers with CRaC support: startup study results Additionally, in certain cases, Alpaquita Containers with CRaC support may result in final images that have a 10% smaller footprint (you can read more about the experimental setup and conclusions in a dedicated article). Spring Boot Petclinic and Alpaquita Containers with CRaC: image footprint study results Looks impressive, doesn’t it? Follow our tutorial on using CRaC with Java in a container and start experimenting with the feature! Which applications need CRaC The CRaC is most beneficial for applications characterized by short runs, frequent restarts, deployment to multiple replicas, possibility to perform a training run. If Operations engineers allocate too little memory to their instances, the application starts up slowly, leading to increased costs, especially If the enterprises use cloud services where they pay for the time your code executes. But if the instances are too large, companies overpay for resources that are never utilized. Additionally, when a Java application goes through the standard startup and warmup processes, the CPU consumption tends to be higher than in the stabilized state. However, in the case of using CRaC, applications will start almost instantly and at peak performance, minimizing the latency and optimizing resource consumption. Main considerations when using CRaC CRaC represents a stateful approach as it preserves the exact state of a running application at a given time, together with information about the Java heap, native memory, JIT-compiled code, settings, etc. As a result, the snapshot may contain sensitive data. Developers should keep that in mind when working with the technology.They should either carefully assess how the snapshot files are created, stored, and accessed, or ensure that the snapshot is taken at the moment when JVM doesn’t store any sensitive information. In addition, developers must consider possible issues with randomness. Since the java.util.Random seed is created upon initialization, the pseudo-random numbers upon restore will be predictable. One way to mitigate this issue is to create a new seed in an afterRestore() method. However, a better solution is to use java.security.SecureRandom, that provides more reliable random numbers generation. With SecureRandom, which is integrated into CRaC, the SecureRandom seed (along with NativePRNG) will be cleaned and the random operations will be locked before taking the snapshot. The lock is removed in an afterRestore() method, ensuring secure random number generation after snapshot restoration. Download JDK builds with CRaC and start experimenting! The CRaC API helps you to minimize the startup of your instances and reduce resource consumption without sacrificing the JIT capabilities for further performance optimization if required. With production-ready containers from BellSoft, there is no need to adjust the JDK or the OS to work with CRaC API, allowing you to focus on implementing the feature into your workloads. Visit the Liberica JDK Download Center to get a JDK build with CRaC or select a container image on our Docker Hub repository. And if you have any questions about the functionality, feel free to contact us, and our engineers will be happy to assist you! Contact us - [Liberica JDK 8u412, 11.0.23, 17.0.11, 21.0.3, 22.0.1 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u412-11-0-23-17-0-11-21-0-3-22-0-1-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 8u411, 11.0.22.0.1, 17.0.10.0.1, and 21.0.2.0.1 CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u412, 11.0.23, 17.0.11, 21.0.3, and 22.0.1 with non-critical fixes and general improvements. The release contains 904 fixes and backports overall. BellSoft participated in eliminating 8 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. [ link | Download Liberica JDK | https://bell-sw.com/pages/downloads ] The summary of fixes 10 security issues (CVEs) fixed. 23 total security fixes (+ 14 additional non-security fixes) in CPU release: in Liberica 6u421: 2 security fixes + 9 additional fixes; in Liberica 7u421: 2 security fixes + 4 additional fix; in Liberica 8u411: 5 security fixes; in Liberica 11.0.22.0.1: 6 security fixes + 1 additional fix; in Liberica 17.0.10.0.1: 4 security fixes; in Liberica 21.0.2.0.1: 4 security fixes. In addition, PSU releases include a total of 867 bugs and backports fixed: in Liberica 8u412: 5 security fixes (+ 5 in FX) + 48 additional fixes (+ 20 in FX); in Liberica 11.0.23: 6 security fixes (+ 5 in FX) + 247 additional fixes; in Liberica 17.0.11: 4 security fixes (+ 5 in FX) + 222 additional fixes (+ 14 in FX); in Liberica 21.0.3: 4 security fixes (+ 6 in FX) + 264 additional fixes (+ 12 in FX). in Liberica 22.0.1: 4 security fixes (+ 6 in FX) + 64 additional fixes (+ 22 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-41993 7.5 javafx web network high none required unchanged high high high CVE-2024-21094 3.7 hotspot compiler network high none none unchanged none low none CVE-2024-21085 3.7 core-libs ava.util network high none none unchanged none none low CVE-2024-21011 3.7 hotspot runtime network high none none unchanged none none low CVE-2024-21068 3.7 hotspot compiler network high none none unchanged none low none CVE-2024-21012 3.7 core-libs ava.net network high none none unchanged none low none CVE-2024-21003 3.1 javafx graphics network high none required unchanged none low none CVE-2024-21005 3.1 javafx graphics network high none required unchanged none low none CVE-2024-21002 2.5 javafx graphics local high none required unchanged none low none CVE-2024-21004 2.5 javafx window-toolkit local high none required unchanged none low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 22 CVE-2023-41993 • • • • • CVE-2024-21094 • • • • CVE-2024-21085 • • CVE-2024-21011 • • • • • CVE-2024-21068 • • • • • CVE-2024-21012 • • • • CVE-2024-21003 • • • • • CVE-2024-21005 • • • • • CVE-2024-21002 • • • • • CVE-2024-21004 • • • • • Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [BellSoft levels the performance of JDK 8 up to JVM 17 with Liberica JDK Performance Edition](https://bell-sw.com/blog/bellsoft-levels-the-performance-of-jdk-8-up-to-jvm-17-with-liberica-jdk-performance-edition/): We are happy to announce the release of Liberica JDK Performance Edition 8, which couples JVM 17 and JDK 8 and immediately increases the performance of JDK 8-based projects up to 10% without major code changes! Table of Contents Staying on old JDK is costly, upgrading is resource-consuming Get the JVM 17-level performance without upgrading JDK 8 Essential enhancements in liberica-perf 8 Stay on JDK 8 and profit from your deployments Staying on old JDK is costly, upgrading is resource-consuming Despite being the most popular Java version, JDK 8 is in deep maintenance, meaning that most performance improvements and new features aren’t backported to this OpenJDK branch. Consequently, The hardware is often underutilized because older JDK versions can’t use it as efficiently as the newer ones. The cloud costs increase due to inefficient containers. User dissatisfaction grows because the application performance can’t be improved to meet user needs. At some point, tuning garbage collection and adjusting the runtime flags stops yielding results, and the only way to increase application performance is to migrate to a newer JDK version. But upgrading the JDK version for the enterprise project is complicated and costly because Numerous changes were introduced to the platform since JDK 8, which may require significantly rewriting the application code. An enterprise project is usually tightly bound to specific libraries and framework versions, so updating them necessitates even more code adjustments. All the while your developers solve the compatibility issues, application performance may be deteriorated to a great extent. However, there’s a way out of the vicious circle: you can stay on JDK 8 and get the performance of newer Java versions with Liberica JDK Performance Edition. Get the JVM 17-level performance without upgrading JDK 8 Liberica JDK Performance Edition 8 (liberica-perf for short) is a version of Liberica JDK based on JDK 8 and HotSpot JVM 17. It enables the enterprises to preserve the project structure based on version 8 and at the same time, enjoy the performance benefits of modern JVM. Liberica JDK Performance Edition seamlessly integrates the core JVM and HotSpot from JDK 17 directly into JDK 8 or 11 builds, so you’ll change only one component of your stack as opposed to full-fledged migration to a newer JDK version where you have to change everything. Java stack changes Therefore, you can migrate your services to liberica-perf 8 with little to no code changes and receive an instant performance boost, which will enable you to optimize hardware utilization and cloud resources consumption, reducing the cost of development: Low-latency garbage collectors (sub-millisecond pauses, terabyte heaps) Up to 10% faster startup 85% better compression speed 113% better decompression speed BellSoft will support JDK 8 until March 2031, so you can plan the migration at your own pace without taking up your developer’s time all at once. More about performance gains with Liberica JDK Performance Edition Essential enhancements in liberica-perf 8 Garbage collection improvements: Production-ready low-latency ZGC and Shenandoah GC absent in OpenJDK 8; Enhanced G1GC with new features such as Parallel Full GC, concurrent cleanup, and heap allocation monitoring. Key runtime and compiler improvements: AppCDS and Dynamic AppCDS absent in OpenJDK 8; NUMA-aware allocation absent in OpenJDK 8; Enhanced logging framework; Enhanced zlib for faster compression and decompression. Refer to the documentation for a complete list of changes, including added and improved features in Liberica JDK Performance Edition 8. Liberica JDK Performance Edition can be used for development and execution of Java applications on Linux-based headless or GUI systems. The builds are supported on Intel processors. The installation procedure is as simple as with any Liberica JDK binary. In most cases, you won’t have to change anything in your code. In a few cases, you might have to switch a few runtime options due to the fact that some options were added to or removed from version 17. Go to Liberica JDK Performance Edition User’s Guide BellSoft engineers will help you to perform necessary adjustments if required for effortless integration of liberica-perf into your project. Stay on JDK 8 and profit from your deployments With Liberica JDK Performance Edition 8, you can elevate the performance of your application to meet modern standards, efficiently reducing the cost of development and increasing user satisfaction, and all of that without migrating to a newer JDK version. Customers who already have a Liberica JDK Subscription receive Liberica JDK Performance Edition 8 for free together with other solutions for Java development. So if you are already a BellSoft client, you are one click away from solving the migration issue! If you would like to know more about the services we provide with Liberica JDK, contact us, and we will be happy to answer all your questions. Contact us - [BellSoft levels the performance of JDK 8 up to JVM 17 with Liberica JDK Performance Edition](https://bell-sw.com/news/bellsoft-levels-the-performance-of-jdk-8-up-to-jvm-17-with-liberica-jdk-performance-edition/): San Jose, Calif., April 23, 2024 /PRNewswire/ -- JDK 8 is still one of the most popular LTS Java versions. A widespread reason is the high financial and resource input required from companies for an upgrade to a newer LTS release. Meanwhile, the JVM in JDK 8 has lower performance, making your cloud use inefficient and therefore disappointing your customers. The BellSoft team is familiar with this issue and is proud to present Liberica JDK Performance Edition 8 as a solution to help enterprises gain time and to use cutting-edge Java functionalities. How Liberica JDK Performance Edition 8 benefits your JDK 8-based applications Liberica JDK Performance Edition 8 is a commercial solution bringing JDK 17 performance to your JDK 8. Namely, it delivers: Low-latency garbage collectors; 10% faster startup; 85% better compression speed; 113% better decompression speed. More detailed information about technical advancements for your JDK 8 is hidden in Garbage collection improvements: Production-ready low-latency ZGC and Shenandoah GC absent in OpenJDK 8; Enhanced G1GC with new features such as Parallel Full GC, concurrent cleanup, and heap allocation monitoring. Key runtime and compiler enhancements: AppCDS and Dynamic AppCDS absent in OpenJDK 8; NUMA-aware allocation absent in OpenJDK 8; Enhanced logging framework; Enhanced zlib for faster compression and decompression. According to Alex Belokrylov, BellSoft's CEO, "BellSoft is committed to delivering modern and sustainable Java features to enterprise development. Liberica JDK Performance Edition 8 is an artificial bridge we designed to enrich your current JDK 8 with fueled JDK 17 functionalities and guarantee significant cost reductions for JDK development, operation, and cloud usage while the shift to the newer LTS versions is on its way". BellSoft engineers are ready to help you with effortless integration of Liberica JDK Performance Edition 8 into your project. About BellSoft BellSoft delivers the most complete Java experience with a more secure, reliable, and cost-effective approach to application development on any platform and in any environment. BellSoft is one of the leading contributors to the OpenJDK, and the only vendor that supports current LTS Java versions, legacy JDK 6 & 7 and Liberica NIK. Liberica JDK is the runtime of choice for VMware, Spring Framework, JetBrains, and millions of users worldwide. For more information, visit www.bell-sw.com. - [Liberica Native Image Kit 23.0.4, 23.1.3, and 24.0.1 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-4-23-1-3-and-24-0-1-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.4 for JDK 17, 23.1.3 for JDK 21, and 24.0.1 for JDK 22 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-41993 7.5 javafx web network high none required unchanged high high high CVE-2024-21094 3.7 hotspot compiler network high none none unchanged none low none CVE-2024-21085 3.7 core-libs ava.util network high none none unchanged none none low CVE-2024-21011 3.7 hotspot runtime network high none none unchanged none none low CVE-2024-21068 3.7 hotspot compiler network high none none unchanged none low none CVE-2024-21012 3.7 core-libs ava.net network high none none unchanged none low none CVE-2024-21003 3.1 javafx graphics network high none required unchanged none low none CVE-2024-21005 3.1 javafx graphics network high none required unchanged none low none CVE-2024-21002 2.5 javafx graphics local high none required unchanged none low none CVE-2024-21004 2.5 javafx window-toolkit local high none required unchanged none low none Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [How to create Java container images on a developer machine for other architectures](https://bell-sw.com/blog/how-to-create-java-container-images-on-a-developer-machine-for-other-architectures/): Most software tools are targeted at the CPU architecture (and sometimes, the operating system) of the machine in use. A virtual machine is a viable solution if you want to build container images for an architecture different from the one you use for local development. With a virtual machine, you can create a container for x86_64 on an Apple Silicon mac. Or if you have ARM-based Graviton Linux instances in AWS, you can create a container for ARM on an x86 machine on Windows. Either way, a virtual machine is a great helper if you have to push an image to production urgently skipping the remote build systems. In our previous guide, we installed a virtual machine on an M-series mac computer. In this article, we will look into building a container image for a Java application and running it inside Docker in a VM on an ARM-based mac. We will take Alpaquita Linux as a Linux distribution because it is not only a small OS for containers, it can be used for server development as well. Table of Contents Create a virtual machine on macOS Enable directory sharing with a guest Linux Create a virtual machine on Windows Install JDK and Docker in the virtual machine Build a container using buildpacks Run the container inside the virtual machine Create a virtual machine on macOS We will use UTM as virtualization software for our macOS example. Refer to our previous guide to install it if you haven’t done it yet. Before we proceed, we need to download an Alpaquita Linux VM image to use with our virtual machine. We provide special .qcow2 images suitable for UTM and other QEMU based VM managers so you don’t have to go through a lengthy configuration process. Go to Alpaquita Download Center, choose a musl-based .qcow2 bundle, and download it. Open UTM, click Create a New Virtual Machine. In the next window, choose Emulate because we want to run Linux for a different architecture. Select Other and Skip ISO Boot. Next, adjust the hardware settings. We allocated 8 GB of memory to our VM. Normally, a VM doesn’t need that much, but if we want to build native images, the process is very CPU demanding. We also allocated four cores: as a rule, you should give your VM half of the cores your machine utilizes. Hardware settings Next, specify the size of the drive for VM data. 10 GB should be enough. After that, specify the path for a shared directory. Verify the settings in the last window and press Save. Your new VM should be successfully created. Created VM We need to make some additional adjustments before running the virtual machine. Right click on the VM on the left-side panel and select Edit. Here, go to System first to choose a CPU. Select Intel Core i7 9xx (Nehalem Core i7, IBRS update) in the drop-down menu. Choosing CPU Next, go to QEMU and unselect the UEFI Boot. Unselecting UEFI The last thing to do is to create a new IDE Drive to boot our .qcow2 image. Delete the IDE Drive that boots the ISO image, select New and specify the path to the Alpaquita Linux bundle. Setting the IDE drive That’s it! Run the Alpaquita VM. It will load quite rapidly thanks to the predefined settings. Finally, it will ask you to enter a login and password. Enter alpaquita for login and alpaquita for password. An optional step is to enable access to the VM from macOS Terminal using SSH so that you can connect to the VM similar to other remote systems. For that purpose, add the openssh package with the following command: sudo apk add openssh-server Start the service with sudo service sshd start To find out the IP address the virtual machine uses, run ifconfig The inet addr line contains the IP address you need. Open the Terminal and run ssh alpaquita@ Where is the IP you found in the config (in our case, the command was ssh alpaquita@192.168.64.8). Enter login and password for alpaquita upon the prompt. You can now work with your VM from the Terminal! Enable directory sharing with a guest Linux We will enable QEMU directory sharing by means of VirtFS. Alpaquita Linux supports 9pfs, but we need to add specific OS modules with sudo apk add linux-lts-extra-modules After that, let’s create a mount point with sudo mkdir /mnt/host Enable sharing with sudo mount -t 9p -o trans=virtio share /mnt/host -oversion=9p2000.L It is also possible to modify /etc/fstab and automatically mount the share on startup with the following command: share /mnt/host 9p trans=virtio,version=9p2000.L,rw,_netdev,nofail 0 0 Finally, place the guest ownership into a file attribute to avoid the “access denied” error: sudo chown -R alpaquita:alpaquita/mnt/host Create a virtual machine on Windows Running Linux on a Windows-based machine is extremely easy thanks to WSL, Windows Subsystem for Linux that enables the developers to work with Linux without setting up a separate virtual machine. If you don’t have WSL installed, make sure that your Windows instance satisfies system requirements for WSL, and then run the following command in an administrator PowerShell or Windows Command Prompt: wsl.exe --install BellSoft provides a dedicated Alpaquita WSL installer for Alpaquita Linux. Go to Alpaquita Download Center and download the .appx package. After that, open the downloaded file and click Install in the Install Alpaquita window. Alternatively, you can install Alpaquita using Windows PowerShell. For that purpose, run the following command specifying the path to the file: Add-AppxPackage -Path "AppFilePath" That’s it, you successfully installed Alpaquita in WSL! You can later start Alpaquita in Powershell using the following command with the distribution name: wsl.exe -d Alpaquita-Glibc No additional steps to enable directory sharing are required because you can access the local machine’s file system from within the Linux Bash shell: the local drives mounted under the /mnt folder. Install JDK and Docker in the virtual machine The next step is to install Java and Docker. Alpaquita repository contains packages with various Liberica JDK versions, including the latest minor versions with the newest patches and fixes. Liberica JDK is recommended by Spring and is the default runtime in Spring buildpacks, so it is perfect for our Spring Boot example. To install Java in the VM, run sudo apk add liberica17-lite-jdk This will install Liberica JDK Lite optimized for cloud. If you want to use another JDK version, run apk search liberica and select the necessary package from the list. Next, install Docker with sudo apk add docker docker-openrc This will install Docker with OpenRC packages. Start Docker with sudo service docker start Verify that docker has started with docker --version Docker version 25.0.3, build 4debf41 Now, we need to add a new user to the Docker service to manage it as a non-root user. For that purpose, run sudo addgroup alpaquita docker After adding a new user, we have to restart our VM. At this point, you can run the following command to automatically start the Docker service upon the VM start: rc-update add docker Restart the VM and login to it again. Repeat the steps with mounting the directory and starting the Docker service if necessary. Build a container using buildpacks We came right up to containerizing a Java application inside a VM! We will use a Spring Petclinic demo application as an example so pull the project from the GitHub repository and move it to the shared directory. Go to the application directory from the Terminal where your VM is running. To build a container using Paketo buildpacks, run the following command: ./mvnw -Dmaven.test.skip=true spring-boot:build-image This command will skip the tests and build a container image of our application. All you have to do now is wait: Maven will have to pull necessary dependencies into a local repository and then build the image. You can create a shared directory for .m2 to reuse the local Maven repository on your mac. This will accelerate the build process. After the build is complete, verify the image was created with docker images You should see the information about the image on the list. Run the container inside the virtual machine The final touch is to run our container from the CM so that we can access it from the browser on the host system. For that purpose, run docker run --rm -it -p 8080:8080 [image-name] Where [image-name] is the name of your image that was given to it automatically by Docker. The application will listen on the 8080 port. We used an M1-based MacBook Air, and the application took 178 seconds to start. Don’t be surprised: emulation is a very resource-demanding process. If you use an M2- or M3-based mac, the process will be faster. Open your favorite browser and go to [VM-IP-address]:8080. You should see the following page: Spring Petclinic home page Congratulations, you have successfully built a Docker container image in the Linux VM! If zou would like to get useful tips, recommendations, and tutorials on Java development and deployment, subscribe to our newsletter! - [Compilation in Java: JIT vs AOT](https://bell-sw.com/blog/compilation-in-java-jit-vs-aot/): Java applications are compiled just-in-time by default. However,GraalVM Native Image made it possible to compile Java programs ahead-of-time. What’s the difference between these two approaches? Is it better to stick to tried-and-true JIT compilation or plunge into the brave new world of AOT? Read on to find out! Regardless of what you choose, Bellsoft developed two solutions aimed at helping you overcome some known drawbacks of the two so you’ll be ahead of the game in any case. If you build Spring Boot applications, check out Alpaquita Containers based on lightweight Alpaquita Linux and Liberica JDK recommended by Spring: these containers will help you save up to 30% of RAM! Table of Contents How compilation in Java works What is a JIT compiler Alpaquita Containers with CRaC for faster startup and optimized footprint HotSpot vs GraalVM What is an AOT compiler Liberica Native Image Kit with adds-on for performance and debugging JIT vs AOT: Summary How compilation in Java works Program compilation is the process of translating a program written in a high-level programming language (Java in our case) into low-level machine code understandable for a computer. The resulting machine code contains binary machine instructions, which can be decoded by the CPU. Compilation is performed by a compiler — a program that converts the source code into machine code in a most efficient way. A compiler can perform various optimizations to speed up the program execution or reduce resource consumption. As Java is platform independent language, the source code is first converted into the JVM bytecode, which is then interpreted by the compiler of a JVM installed on the computer into platform dependent machine code. This additional stage enables the implementation of Java’s WORA (write one, run anywhere) principle. The source code is translated into machine code before or during the program execution, and this is where JIT and AOT come into play. What is a JIT compiler In the case of Just-in-Time compilation, also known as dynamic compilation, JVM bytecode is converted into machine code by the JVM after the program has started, i.e., at run time. The goal of a JIT compiler is to produce highly performant code. Firstly, it analyzes the code to decide which parts are used more often (so called “hotspots”) and deserve being optimized, which operating system the code runs in, and so on. Secondly, it applies optimizations it deemed necessary: global, local, control flow, etc. As a rule, the better the resulting code, the more optimizations are performed, the longer is the “warm up time,” which is the delay in the actual program execution. The intensity of optimizations may vary depending on the program. HotSpot, the most popular JVM, provides two compilers, C1 and C2: C1 is a client compiler that starts optimizing the code immediately and performs simple optimizations. As a result, it provides faster startup, but less optimized code. C2 is a server compiler that observes the running code for a while to collect the data for profiling, and only after that starts optimizing it. The warmup time increases, but the code is more performant than the one produced by C1. In addition, a JIT compiler can perform adaptive optimization and dynamically recompile code portions during app’s execution in case of changes. For instance, methods not used before are suddenly required. JIT compilation provides performant code for a specific use case and environment it runs in. In addition, you can use a wide variety of Garbage Collectors with JIT, including low-latency collectors such as ZGC present in newer Java versions, and thus reduce latency or increase throughput depending on your needs. On the other hand, JIT compilation has several drawbacks: The warmup phase may take several minutes, all the while the application processes fewer requests than when at a stable state, leading to higher latency in the beginning. The worst part is that this process repeats every time you restart the application. The application uses more resources during the warmup. As a result, you allocate more memory to your instances than necessary. The resulting container images are “bulky” because they include the compiled code plus the JVM. The advantages and drawbacks of JIT compilation can be summarized as follows. JIT advantages JIT disadvantages Highly performant code thanks to various performance optimization techniques Extended warmup period Dynamic performance optimization when the application is running Higher overhead at the start Familiar debugging and monitoring tools Bigger container images with JVM Alpaquita Containers with CRaC for faster startup and optimized footprint What if you could warm up the application, save its state to a file, and then restore it from the file from the moment it was paused much like in a computer game? Great news is that you can do that with Alpaquita Containers that support the Coordinated Restore at Checkpoint (CRaC) API. CRaC enables the developers to take a snapshot of a running Java application, replicate this snapshot among cloud instances, and restore the application at peak performance. This way, the startup and warmup times are reduced from minutes to milliseconds. In addition, there’s no memory overhead because the application state is stabilized. Therefore, two major drawbacks of JIT compilation are essentially eliminated. And a lightweight Alpaquita Container with minimalistic, Alpine-inspired Alpaquita contributes to the overall container size reduction. Spring Boot Petclinic and Alpaquita Containers with CRaC: startup study results The important thing is that the JIT compiler is there, so dynamic performance optimization is still possible. Alpaquita Containers with Alpaquita Linux and Liberica JDK provide out-of-the-box support for CRaC API, so you don’t have to tune JDK or OS to work with the feature. To learn more about the feature, refer to the article What is CRaC. And if you are ready for experiments, follow our guide on how to use CRaC with Spring Boot in a Docker container. HotSpot vs GraalVM It is important to mention that the HotSpot compiler is not the only JIT-compiler in the Java ecosystem. There’s also OpenJ9 developed by IBM: you can study the comparison of HotSpot vs OpenJ9 performance if you are curious. But as opposed to HotSpot and OpenJ9 written in C/C++, there’s also GraalVM, which is a JDK written in Java. As opposed to HotSpot JIT compilation, GraalVM’s JIT compiler offers additional optimizations so Graal outperforms HotSpot JVM in some cases. In addition, GraalVM offers a Truffle framework for running non-JVM-based applications allowing for efficient communication in multilingual projects. But the most interesting part is that GraalVM also includes the Native Image technology that enables the Ahead-of-time compilation of Java applications, which returns us to the main topic of our article. What is an AOT compiler Ahead-of-time or static compilation happens before the program execution, i.e., at build-time. The GraalVM AOT compiler performs static analysis of the code under the closed-world assumption meaning that it assumes that all classes that will be required at run time are reachable at build-time (the pitfalls of this approach are discussed below). The compiler translates bytecode into machine code specific to the operating system and architecture. All necessary classes are initialized, and their code is loaded into a single native executable together with required library classes and statically linked code from the JDK. The resulting native image starts up almost instantly because there is no need to search for hotspots and perform bytecode interpretation at runtime. An AOT compiler eliminates unused code and dependencies, and coupled with the fact that the executable file doesn’t need a JVM to run, it helps to drastically reduce memory consumption, accelerate startup up 1/10 s, reach peak performance immediately without warm-up, increase the security thanks to a smaller attack surface. Another substantial advantage of AOT compilation is that the code of the resulting native executable is very hard to reverse engineer, especially when run through an obfuscator before compiling a native image. It adds an extra level of IP protection. As tempting as it may sound to migrate the project to Native Image and forget about long warmup and “bloated” containers, the technology has some drawbacks, and the migration presents a hard-to-ignore caveat. As far as the drawbacks are concerned: Building a native image is extremely resource demanding as you need to allocate several gigabytes of memory to the process. As the resulting executable doesn’t contain a JVM, further performance optimization is impossible. GraalVM offers a limited number of garbage collectors: SerialGC in GraalVM Community Edition and G1GC in GraalVM Enterprise Edition. Suppose you don’t expect a sudden surge in requests and are satisfied with Native Image performance as it is. AOT compilation of an existing project may be challenging because Native Image doesn’t support the dynamic features of Java: Reflection, JNI, Dynamic Proxy, etc. So you have to provide a workaround — either rewrite the code or provide all metadata for required libraries in the JSON file using Tracing Agent for AOT compiler to “take them in.” If you are curious, here’s an example of dealing with Reflection in a Spring Boot application when migrating to a Native Image. To sum up, the pluses and minuses of AOT compilation are provided below. AOT advantages AOT disadvantages Almost instant startup No dynamic performance optimization Smaller container images Incompatible with dynamic features of Java Smaller attack surface Limited range of garbage collectors Hard to reverse engineer Challenging diagnostics Liberica Native Image Kit with adds-on for performance and debugging To elevate the overall performance of native images, you can use Liberica Native Image Kit (NIK), a GraalVM CE-based native-image compiler developed and supported by BellSoft. We added a ParallelGC implementation to Liberica NIK that can significantly improve latency with native images (by 10–40% according to our studies). macOS users can conveniently debug native images thanks to the addition of this feature to Liberica NIK. Liberica NIK is used by default with Cloud Native Buildpacks and recommended by Spring as a Native Build tool, which promises smooth integration of the technology with your Spring Boot project. JIT vs AOT: Summary To sum up, JIT and AOT compilers offer different approaches to optimizing Java application performance. JIT compilation enables higher overall performance and dynamic performance optimization. However, it may take several minutes for an application to warm up, which is not optimal for certain cloud deployment scenarios. JIT compilation is also associated with memory overhead and higher memory consumption overall. Mitigating these drawbacks is possible with Alpaquita Containers supporting CRaC API. AOT compilation allows for the creation of native images with almost instant startup. A single native executable can be smaller due to the absence of the JVM, contributing to reduced memory consumption. On the other hand, the Native Image technology doesn’t provide dynamic performance optimization and offers a scarce amount of GC implementations. To smooth out this drawback, you can use Liberica Native Image Kit that includes ParallelGC. In addition, AOT compilation happens under the closed-world assumption, making the migration difficult. There’s never a one-size-fit solution. AOT and JIT work their magic in different scenarios, so to understand which solution is best for your project, you should match your SLOs with the advantages of these approaches and evaluate the risks related to their drawbacks. - [What are zero-day vulnerabilities and how to minimize zero-day attacks](https://bell-sw.com/blog/what-are-zero-day-vulnerabilities-and-how-to-minimize-zero-day-attacks/): 0-day vulnerabilities pose an immense challenge to software developers and enterprise IT teams because how can you fight something you don’t know exists? In this article, we lift the veil on the dark nature of zero-day vulnerabilities and how software developers and hackers try to outrace each other in finding them. Table of Contents What is a zero-day vulnerability Zero day vulnerabilities, exploits, and attacks Zero-day vulnerability lifecycle How to minimize zero-day attacks Reduce attack surface Reduce mean time to patch (MTTP) Perform regular monitoring for anomalies Prevent known vulnerabilities from nesting in your app Zero-day vulnerabilities are unavoidable, but we can minimize risks of attacks What is a zero-day vulnerability According to the combined Google’s Threat Analysis Group (TAG) and Mandiant report, 97 zero-day vulnerabilities were exploited in 2023, with third-party libraries and components being the primary attack surface. As per the 2024 Data Breach Investigations Report by Verizon, 14% of breaches involved vulnerability exploits, which is three times more than the previous year. Zero-day (or 0-day) vulnerabilities are security flaws that have been found exploited in-the-wild or publicly disclosed before they were fixed by software vendors. “Zero-day” refers to the number of days a vendor has to fix the vulnerability before it gets exploited by malicious actors. Not every vulnerability is considered zero-day, it becomes one when there’s no patch available at the moment of discovery. How do zero-day vulnerabilities land in software? They typically sneak into a program with code changes: the more changes are made, the more difficult it is to spot a flaw. Imagine 10,000+ lines of code introducing a new feature. Reviewers can’t manually take apart every line in search of a flaw, and code analysis tools don’t always notice bugs constituting vulnerabilities despite doing a great job at checking code security. Zero-day vulnerabilities are especially dangerous because the vendor is unaware of their existence. As these flaws are usually used in toolkits for surveillance, espionage, or highly targeted attacks, stakeholders are interested in exploiting zero-day vulnerabilities for as long as possible. So, it is more lucrative for hackers to sell information about zero-day vulnerabilities than to put them into traditional malware used in mass campaigns, which will be discovered the next day. Another common practice is to use zero-day vulnerabilities in ransomware. Search for zero-day vulnerabilities is a constant race between hackers and software developers. Ironically, they use similar tools and methods to identify vulnerabilities, including but not limited to static code analyzers and fuzzers. And to answer the eternal question of which software is safer, open or closed source. On the one hand, it is indeed easier to find a flaw in the generally accessible source code. However, it doesn’t mean that closed-source code can’t be analyzed for flaws. For instance, black-box fuzzing is used to observe application behavior at runtime to spot vulnerabilities without examining the source code. Also, open-source software usually has a vast community of developers and users that encounter and report bugs or purposefully hunt down vulnerabilities. So, the risk of zero-day exploits is higher with open-source software, but so are the chances that the flaws will be identified and patched before hackers get to them. Zero day vulnerabilities, exploits, and attacks There are three terms related to the zero-day concept: Zero-day vulnerability is a flaw that exists in the software unknown to the software vendor. Zero-day exploit is a code that uses the software vulnerability to compromise the system. Zero-day attack is an attack on a system that uses the vulnerability not yet patched by the vendor. In addition, zero-day vulnerabilities are not to be confused with one-day vulnerabilities, the vulnerabilities for which the patch exists but hasn’t been deployed to the systems using the software yet. “One-day” refers to the period between patch release and integration into the systems, but as this period may be prolonged, such vulnerabilities are also called n-day vulnerabilities. To create an exploit, hackers reverse engineer the patch to see the issue it solves or use proof-of-concept code released by other actors after the patch is made available. Zero-day vulnerability lifecycle As mentioned before, vulnerabilities are introduced into the program with code changes. Vulnerabilities can lurk in software for months or years before being uncovered (for instance, the notorious Log4Shell in the Log4J Java library existed since 2013 but was discovered in 2021). Their further life depends on who found the flaw. Zero-day vs n-day vulnerabilities If bad actors get to the vulnerability first, they define the attack vector — the code that can be affected after inputting malicious data — and prepare an exploit. The exploit is used to attack a vulnerable system. What if the vendor discovers the vulnerability first (thanks to their cybersecurity team or a bug report from users or customers)? A patch is made and introduced with a software update in this case. The vendor informs users about the patch. Users must update the software product as soon as possible to apply the patch. At the same time, information about the vulnerability becomes public, which is when it can be analyzed and exploited as an n-day vulnerability. How to minimize zero-day attacks Unfortunately, unknown zero-day vulnerabilities may appear in your project. Fighting them is hard because they are like Schroedinger’s cat, except that, in this case, you don’t even know whether the cat is in the box. Traditional vulnerability scanners and antivirus solutions are no great help because there are no signatures yet. However, you can proactively guard your project from zero-day attacks. Reduce attack surface The attack surface comprises all attack vectors a hacker can use to access your system. So, reducing the attack surface will lower the chance to find a vulnerable object or dependency within the attack surface. Implement the principle of least privilege: each user should have access only to resources and system parts necessary to do their jobs. Implement zero trust policy: users should have access to the system only if they are properly authenticated. Segment the network and use good firewalls: this way, you can block hackers from gaining control of the whole system. Reduce mean time to patch (MTTP) Mean time to patch (MTTP) is the average time an enterprise applies patches to vulnerabilities. According to the 2024 Data Breach Investigations Report, patching starts picking up after 30 days only, and 8% of patches still need to be implemented by the end of the year. Reducing MTTP is critical to protect the infrastructure from attacks. Luckily, a proper patch management system helps implement patches sooner and limits the exposure window. Working with the vendor to receive fixes and patches for your project as soon as possible is also advisable. For instance, BellSoft offers its customers SLA for a security patch max. 48 hours. Perform regular monitoring for anomalies Although you may not be aware of 0-day vulnerabilities, you can spot unusual activity in your network that indicates an attempt at exploiting. For that purpose, continuous monitoring of traffic and log analytics is performed. To facilitate the task, you can use a network intrusion protection system (NIPS) or a next generation antivirus (NGAV) that spot suspicious and unusual behavior. Prevent known vulnerabilities from nesting in your app Even when a vendor releases a patch, you have to meet them halfway and apply it as soon as possible. However, many enterprises are in no haste to update project dependencies or don’t update them at all. As a result, the project contains numerous known vulnerabilities, which makes it highly susceptible to hacker attacks. By the way, Java services are the most affected by third-party vulnerabilities, according to the DataDog’s State of DevSecOps Report. How do you remedy the situation? Implement a Software Bill of Materials (SBOM) that lists all dependencies used in your application, their versions, and known vulnerabilities. This way, you will always know what is happening behind the scenes of your project. Besides, some legislations already demand the adoption of SBOM in some cases, and nonconformity with legislative requirements may lead to heavy fines or other grave consequences. Zero-day vulnerabilities are unavoidable, but we can minimize risks of attacks To sum up, zero-day vulnerabilities will inevitably appear in software because of the complex coding processes. They are especially dangerous because nobody knows about their existence until they are discovered. However, we can prevent zero-day attacks or at least minimize their risk. Software vendors and developer communities are constantly hunting for and patching vulnerabilities. Some vendors initiate bug bounty programs to involve independent researchers in the hunt. You can implement the tools and best practices mentioned above to protect your project from attacks. And you can also contribute to the zero-day-hunt by reporting bugs you discovered to the vendor or the community. So, let’s consolidate our efforts and work on software security from zero-day of its development! - [New features in Spring Boot 3.3](https://bell-sw.com/blog/new-features-in-spring-boot-3-3/): Spring Boot 3.3 is generally available! The release includes numerous updates, including several new features. We have decided to do some cherry-picking and take a look at the most significant changes, among which the support for Class Data Sharing (CDS) for faster application startup. Table of Contents New Service Connections CDS Support Auto-configuration of Embedded Web Server SSL with SNI Other important improvements Observation Support for Prometheus Client 1.x SBOM Actuator Endpoint New Service Connections Several service connections were improved in or added to Spring Boot: Added support for Apache ActiveMQ Artemis; Added support for the apache/activemq-classic docker image and the ActiveMQContainer testcontainer to the ActiveMQ service connection; Added service connection support for LDAP with the osixia/openldap container; Spring Boot Docker Compose will detect and configure Bitnami containers in addition to official images for several technologies, including but not limited to Elasticsearch, MongoDB, and PostgreSQL. CDS Support Class Data Sharing (CDS) is a JVM feature that helps to minimize the startup of a Java application by creating the archive of already initialized classes, which can be used for further application launches and even shared between JVM instances. Spring Boot already supports extracting layers from the application uber jar with the help of -Djarmode=tools (which supersedes -Djarmode=layeredtools), which enables the developers to accelerate docker pull and container image updates. As far as CDS is concerned, Spring Boot offers a convenient way to create a CDS archive for application (AppCDS) on application exit when you specify two JVM flags: -XX:ArchiveClassesAtExit=application.jsa to create the CDS archive; -Dspring.context.exit=onRefresh to start and immediately exit the application. As a result, the process exits automatically once the ApplicationContext has refreshed, but the lifecycle hasn’t started yet. To create a CDS archive for the application, your JDK must have a base image. To minimize the effort of creating a base CDS archive for JVM, you can use a prepared container image with Liberica JDK and system CDS. This way, you don’t have to use special commands to generate a CDS archive. To use Liberica JDK containers with CDS, visit our Docker Hub repository and select an image with a cds tag. If reducing the application startup time is crucial to you, CDS may be perceived as a go-between for the standard JVM and innovative solutions such as Coordinated Restore at Checkpoint (CRaC) and GraalVM Native Image. There’s no need to change the application code to use CDS, but you may need to adjust the runtime settings and consider several important aspects (such as that the classpath and JVM version must be the same as the ones used when building the archive). We believe that using CDS with Spring Boot apps has exciting prospects, and this topic deserves a separate article, which it is coming soon! You can subscribe to our newsletter so as not to miss it. Auto-configuration of Embedded Web Server SSL with SNI Server Name Indication (SNI), an extension to the TLS protocol that enables the specification of a domain name the client device tries to reach, is now supported when configuring an embedded web server (Tomcat, Netty, or Undertow). Multiple hostnames with unique SSL trust material can be configured declaratively through spring.ssl.bundle properties in the .properties or .yaml file. Other important improvements Observation The following improvements were delivered to Spring Boot observability capabilities: Added support for Micrometer @SpanTag annotation, which enables you to add tags for custom spans; It is now possible to use properties to enable observations for the simple, direct, and stream listener and on the RabbitTemplate; Added support for tagged fields for Brave and OpenTelemetry and support for local fields for Brave; unknown_service is used for OpenTelemetry if no application name is specified to align with the OpenTelemetry specification; Changed the default value for the spring.pulsar.listener.observation-enabled and spring.pulsar.template.observations-enabled properties from true to false; Implemented a new JDK HttpClient based Zipkin sender; Updated Brave to 6.0 and Zipkin to 3.0. Support for Prometheus Client 1.x Spring Boot 3.3 supports Prometheus Client 1.x. Prometheus Client 1.0.0 was a long-expected release of a popular Java metrics library as it includes some ground-breaking changes including but not limited to built-in support for native histograms, seamless integration with OpenTelemetry tracing, and support for OpenTelemetry Exporter. SBOM Actuator Endpoint The spring-boot-actuator module now includes a SBOM endpoint. In addition, the spring-boot-parent-starter includes additional configuration for a more convenient configuration of SBOM plugins. More detailed information about other fixes and enhancements can be found in Release Notes. - [Updating the Java version](https://bell-sw.com/blog/updating-the-java-version/): Updating Java refers to installing a newer build of the same version you currently use. Updating your Java version is key to keeping the foundation of your application safe and stable. Let’s see what types of updates for your Java runtime are available, which Java versions can be updated for free, and how to update your JDK depending on the platform. Table of Contents Why you need to update Java Java updates schedule Critical Patch Update and Patch Set Update: what’s the difference? Which Java versions receive free updates How to know which Java version you use How to update your Java version Windows macOS Linux Package managers Containers Discovery API Why you need to update Java Updating your JDK is as important as updating any other software. The risks of not updating Java include security breaches due to exploits of known vulnerabilities and unstable operation due to bugs. Just a couple of numbers: 10 Common Vulnerabilities and Exposures (CVEs) were discovered and patched in jDk in 2023 alone; 135 CVEs were discovered and patched in JDK 11 between October 2018 and April 2023. Every known vulnerability in your project can serve as an open backdoor for malicious actors. Sometimes, the impact of a vulnerability is so serious it has ripple effects globally. One example of such a vulnerability is Log4Shell, a Remote Code Execution (RCE) vulnerability in the Log4J library with the highest CVSS score of 10, which affected millions of systems worldwide. Even despite the fact that the vulnerability was patches and new versions of Log4J are free of it, the vulnerability is still being exploited because the older versions of this library are deeply rooted into software supply chains and are used sometimes as an indirect dependency. Therefore, Java updates provide you with: Security patches, Bug fixes, Performance optimizations introduced with minor enhancements to the runtime and backports from newer JDK versions (although the older the Java release, the less improvements and features get backported there). Note that if enhancing the performance of your Java application is critical to you, but you are not ready to migrate to a newer Java version, you can take advantage of Liberica JDK Performance Edition that couples JVM 17 and JDK 8 or 11, allowing for instant performance boost without dependency updates and major code changes. Java updates schedule Java updates schedule overview Updating Java means moving within a major release, for instance, from JDK 21.0.1 to JDK 21.0.3. It is not the same as upgrading, which means migrating to a newer version, for instance, from JDK 8 to JDK 17. LTS vs feature releases Updates to the ‘active’ OpenJDK branches, that is, Long-Term Support (LTS) versions and a current feature version, are released quarterly, commonly on the third Tuesday of January, April, July, and October. These updates include security patches for known vulnerabilities and bug fixes. Vendor-specific support lifecycles LTS versions receive updates for several years, but the lifecycle depends on the vendor and the Java version (see a comparative table of JDK support from major vendors here). Feature releases receive updates for six months. Critical Patch Update and Patch Set Update: what’s the difference? There are two types of updates available for your Java runtime: Critical Patch Update (CPU) and Patch Set Update (PSU): CPU updates include patches for CVEs and the most critical bug fixes. They affect a smaller range of Java libraries and so can be integrated into the project without the risk of disrupting the workflow. PSU updates include all patches and fixes from CPU plus non-critical fixes, enhancements, and feature updates. Most OpenJDK release only PSU builds, and three vendors — Oracle, BellSoft, and Azul — provide CPU builds in addition to PSU. Which Java versions receive free updates OpenJDK vendors provide free updates for major LTS versions for commercial and personal use, but the range of versions depends on the vendor. For instance, SAP and Microsoft don’t provide updates for JDK 8. BellSoft releases updates for all LTS versions: 8, 11, 17, and 21. If you use Oracle Java, the situation is a bit more complicated. In short, Oracle Java 8 and 11 public updates are available for personal use only. Starting with Java 17, Oracle releases free updates for personal and commercial use for three years, and after that you must either migrate to the next LTS release or acquire a license to continue receiving updates for Oracle Java used in enterprise. For more information about differences between personal and commercial use, various Oracle Java licenses and Oracle’s pricing policy, refer to the Java licensing overview. What if you use Java 6 or 7? Unfortunately, there are no free updates for these releases, and most vendors don’t support them. But if migration to newer Java versions is off the table for now, BellSoft provides commercial Java 6 & 7 builds with quarterly security updates and 24/7 support service. For Java versions below 6 & 7 there are no updates at all, so we recommend upgrading at least to Liberica JDK 6(7) to receive security patches, or to Liberica JDK 8 if you’d like to get updates until 2031. The BellSoft engineers will be happy to help you with the migration, contact us to learn more! Contact us How to know which Java version you use Regardless of an operating system, the fastest way to check which Java version is currently in use is to run: java -version openjdk version "21.0.3" 2024-04-16 LTS OpenJDK Runtime Environment (build 21.0.3+10-LTS) OpenJDK 64-Bit Server VM (build 21.0.3+10-LTS, mixed mode, sharing) If you get the message “java is not recognized as an internal or external command”, you need to configure a JAVA_HOME variable and add the JDK /bin subdirectory to the PATH. For macOS and Linux: export PATH=/bin:$PATH export JAVA_HOME= For Windows: Select Properties → Advanced system settings → Environment variables. Select 'Path' in System variables, click Edit → New, type C://bin/ Select ‘New’ under System variables, type JAVA_HOME in Variable name, and type the path to JDK installation directory in Variable value. How to update your Java version Note that different JDK vendors provide different delivery channels for their software, so some of the alternatives described below may not be available with your vendor. We use Liberica JDK 21, the latest minor release available at the moment of writing this article, as an example. How to update Java on Windows If you don’t use package managers, there are two convenient ways to update Java on Windows: Download the .msi file and follow the instructions in the JDK Setup Wizard. This will overwrite the existing JDK installation. Alternatively, you can install Liberica JDK silently without user interaction by running msiexec /i bellsoft-jdk21.0.3+12-windows-amd64.msi /qn /quiet /norestart /l*v "Liberica_install.log" The second option is to download the .zip file, unpack JDK and set the PATH and JAVA_HOME environment variables as described in the section above. You may want to delete the previously installed version to save space. You can also use Windows PowerShell to download and unpack the .zip file: Invoke-WebRequest "https://download.bell-sw.com/java/21.0.3+12/bellsoft-jdk21.0.3+12-windows-amd64.zip" Expand-Archive bellsoft-jdk21.0.3+12-windows-amd64.zip -DestinationPath . For more detailed instructions on Liberica JDK installation on Windows, see the relevant Liberica JDK Installation Guide. How to update Java on macOS To update Java on macOS, you can download the .dmg, .pkg, .tar.gz, or .zip package: In the case of the .dmg file, follow the instructions in the installer that pops up when you double-click the file. The existing JDK installation will be overwritten. If you download the archive, unpack the file and set the PATH and JAVA_HOME environment variables as described in the section above. You may want to delete the previously installed version to save space. You can also download and unpack the archive with the following commands: wget https://download.bell-sw.com/java/21.0.3+12/bellsoft-jdk21.0.3+12-macos-amd64.zip unzip bellsoft-jdk21.0.3+12-macos-amd64.zip For more detailed instructions on Liberica JDK installation on macOS, see the relevant Liberica JDK Installation Guide. How to update Java on Linux There are two ways to update Java on Linux: using a repository or Installing the binary manually. If you already installed Java via the Linux repository, you can simply update the repository package index and upgrade all packages or one specific package by specifying its name. For instance, for apt-based systems: sudo apt-get update sudo apt-get upgrade bellsoft-java21 For apk-based systems such as Alpine or Alpaquita: apk update apk upgrade bellsoft-java21 For yum-based systems: sudo yum update sudo yum upgrade bellsoft-java21 For manual upgrade of Liberica JDK on various Linux distributions, refer to the relevant Liberica JDK Installation Guide. Package managers If you installed Java using one of the following package managers: SDKMAN, Homebrew, SCOOP, Chokolatey, winget, update the JDK package accordingly. SDKMAN: sdk upgrade java Homebrew: brew reinstall liberica-jdk21 SCOOP: scoop update liberica21 Chokolatey: choco upgrade libericajdk winget: winget upgrade --id BellSoft.LibericaJDK21 Containers Updating the base image of your containerized application is essential to keep your cloud workloads safe. If you use the specific image version, update it to the newest one in your Dockerfile, for instance, FROM bellsoft/liberica-runtime-container:jdk-21.0.3-stream-musl As BellSoft provides continuous updates to its Liberica Runtime Container images (based on Liberica JDK and Alpaquita Linux), you can use the latest tag or a major version (for instance, bellsoft/liberica-runtime-container:jdk-21-stream-musl), and everytime you rebuild your container, you will have the latest available base image with fixes and patches in the JDK and Linux. Discovery API BellSoft provides the option of browsing and downloading Liberica JDK binaries via REST Discovery API. You can discover: The latest Liberica JDK release, The latest Liberica DK LTS release, Whether your Liberica JDK version is the latest version, A link for downloading without parsing JSON, Supported systems and architectures. To learn more about discovering and downloading Liberica JDK releases via Discovery API, refer to Liberica JDK Product Discovery API. - [How to use CDS with Spring Boot applications](https://bell-sw.com/blog/how-to-use-cds-with-spring-boot-applications/): In the previous article, Dmitry described ways to reduce Java application startup time. Here, I offer you to try out one of them, Class Data Sharing (CDS). It provides a good improvement in startup, not as drastic as with CRaC or Native Image, but it doesn’t necessitate code refactoring. And since Spring Boot 3.3 supports CDS, and BellSoft provides binaries and ready container images with Liberica JDK and CDS, starting with this feature couldn’t be easier! Table of Contents What is Class Data Sharing (CDS) How to use CDS with Spring Boot Create and use the CDS archive on a local machine Peeking under the hood of CDS Create the CDS archive with Spring AOT enabled Create the CDS archive using a container with CDS Use CDS with buildpacks How to Use AOT Cache instead of AppCDS with Java 24 for even faster startup Summary What is Class Data Sharing (CDS) Class Data Sharing (CDS) is a JVM feature aimed at reducing the startup and memory footprint of multiple JVM instances running on the same host. The feature loads a default set of classes from the system Java Archive (JAR) file and stores this data in a file, which is then available as read-only metadata to multiple JVM processes. CDS was first introduced in JDK 5 and received numerous enhancements since then. For instance, JEP 310 in OpenJDK 10 extended on the CDS feature and introduced Application Class Data Sharing (AppCDS), which enables loading application classes into the archive as well. AppCDS is further improved with JEP 350 in OpenJDK 13 that allows for including all loaded application classes and library classes not present in the default CDS archive upon the application exit. Dynamic CDS eliminated the need to do trial runs to create a class list for the application. So when we say below that we create and use a CDS archive with a Spring Boot application, we actually work with AppCDS. How to use CDS with Spring Boot Spring Boot 3.3 offers a convenient way to create a CDS archive. All you need is to specify two options upon application start: -XX:ArchiveClassesAtExit=application.jsa to create an archive of classes and -Dspring.context.exit=onRefresh to start and immediately exit the application after non-lazy beans have been instantiated and InitializingBean#afterPropertiesSet callbacks have been invoked. Note that to run the application with the CDS archive, you must use the same JVM utilized for creating the archive. In addition, the classpath must be the same. If you don’t specify the classpath option, it will consist of the current directory. Create and use the CDS archive on a local machine For this tutorial, I will use the Spring Petclinic demo application. You can use any other project, just make sure that Spring Boot version is 3.3. As for the Java runtime, we will use Liberica JDK recommended by the Spring team. Download Liberica JDK 21 for your platform directly from the website or choose any other installation method described there. Alright, we’re all set. Let’s first create a jar file of our application with: mvn -Dmaven.test.skip=true clean package But we are not going to use an executable jar. Running CDS directly on an executable jar in production is not recommended because running it introduces certain overhead. So, we will take advantage of an exploded jar, which is more efficient and is recommended to be used with CDS by Spring. The command for creating the CDS archive is as follows: java -Djarmode=tools -jar target/spring-petclinic-3.3.0-SNAPSHOT.jar extract java -XX:ArchiveClassesAtExit=./application.jsa -Dspring.context.exit=onRefresh -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar Note that if you use another JDK, you may get a warning similar to -XX:ArchiveClassesAtExit is unsupported when the base CDS archive is not loaded. Run with -Xlog:cds for more info. This means that you will have to create a base image first with -Xshare:dump. We can now use the created archive to launch our application: java -XX:SharedArchiveFile=application.jsa -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar That’s it, two commands only and your application uses the CDS archive! Peeking under the hood of CDS In the previous section, we let Spring Boot and JDK do their magic. But I want to lift the veil over the process of creating the CDS archive and see how many and what classes get dumped there. That being said, modify the command for creating the archive and add logging to analyze the results of CDS implementation later: java -XX:ArchiveClassesAtExit=application.jsa -Xlog:cds=debug:file=log/cds.log -Dspring.context.exit=onRefresh -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar The -Xlog:cds=debug:file=log/cds.log option will log the process of CDS archive creation. Let’s modify the command for launching the application with the archive in a similar fashion. We can add logging to see whether the archive was actually used upon application startup (-Xlog:class+path=debug) and count the number of loaded classes (-Xlog:class+load=info): java -XX:SharedArchiveFile=application.jsa -Xlog:class+load=info:file=log/class-load.log -Xlog:class+path=debug:file=log/class-path.log -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar Great! After the application has started, exit it and check the logs. The cds.log file lists the information about the skipped classes, i.e., classes that didn’t make it into the archive for some reason. For instance: [4.118s][warning][cds] Skipping net/bytebuddy/dynamic/ClassFileLocator$Resolution$Explicit: Old class has been linked [4.118s][debug ][cds] Skipping org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer$$Lambda+0x000000d0014722b0: Hidden class [4.118s][warning][cds] Skipping net/bytebuddy/dynamic/DynamicType$Builder$MethodDefinition$ParameterDefinition$Simple: interface net/bytebuddy/dynamic/DynamicType$Builder$MethodDefinition$ExceptionDefinition is excluded [4.118s][debug ][cds] Skipping org/springframework/data/jpa/repository/query/JpaQueryParserSupport$ParseState$$Lambda+0x000000d001966cf8: Hidden class [4.118s][debug ][cds] Skipping java/lang/invoke/LambdaForm$DMH+0x000000d001b80c00: Hidden class [4.118s][debug ][cds] Skipping java/lang/invoke/LambdaForm$DMH+0x000000d001015c00: Hidden class [4.118s][debug ][cds] Skipping java/lang/invoke/LambdaForm$DMH+0x000000d001b4a000: Hidden class These are Hidden classes, which were introduced in JDK 15 with JEP 371 and represent classes that cannot be used directly by the bytecode of other classes. These classes are not suitable for the CDS archive due to their dynamic nature; Old classes, which are classes from older Java versions. They can typically be found in legacy API libraries; Child classes of other excluded classes. The class-path.log file tells us that the archive was indeed used: [0.005s][info][class,path] bootstrap loader class path=/Library/Java/JavaVirtualMachines/liberica-jdk-21.jdk/Contents/Home/lib/modules [0.012s][info][class,path] Expecting BOOT path=/Library/Java/JavaVirtualMachines/liberica-jdk-21.jdk/Contents/Home/lib/modules [0.012s][info][class,path] Expecting -Djava.class.path= [0.012s][info][class,path] checking shared classpath entry: /Library/Java/JavaVirtualMachines/liberica-jdk-21.jdk/Contents/Home/lib/modules [0.012s][info][class,path] ok [0.125s][info][class,path] Expecting BOOT path=/Library/Java/JavaVirtualMachines/liberica-jdk-21.jdk/Contents/Home/lib/modules [0.125s][info][class,path] Expecting -Djava.class.path=spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar [0.125s][info][class,path] checking shared classpath entry: /Library/Java/JavaVirtualMachines/liberica-jdk-21.jdk/Contents/Home/lib/modules [0.125s][info][class,path] ok [0.125s][info][class,path] checking shared classpath entry: spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar [0.125s][info][class,path] ok How about the number of loaded classes? Run the following command to see how many classes were loaded in total: cat log/class-load.log | wc -l 16242 The same file contains data on files that were loaded from the shared archive. Run the following command to get the number: grep -o 'source: shared' -c log/class-load.log 14391 As you can see, about 88% of classes got into the archive. What about the startup? The mean startup time of the application running without the archive was 3.3 seconds. In turn, the mean startup with the preloaded CDS archive was 1.9 seconds, which means that in this case, CDS yields 42% faster startup. Already great, but we can do even better with Spring AOT! Create the CDS archive with Spring AOT enabled Spring Ahead-of-time optimizations (AOT) are aimed primarily at leveraging the power of GraalVM Natime Image for Spring applications, but they can als help with startup reduction when using the standard JVM. I thought I should give it a try after reading CDS with Spring Framework 6.1 by Sébastien Deleuze. And I was amazed to see for myself how without extensive huffing and puffing with the configs you can get impressive performance boost! Right, to the task at hand. Let’s first enable AOT processing in the pom.xml: org.springframework.boot spring-boot-maven-plugin process-aot process-aot You can also enable AOT compilation without modifying the pom.xml. To do that, you need to run: mvn clean compile spring-boot:process-aot package Now, modify the command for generating the CDS archive in the following way: mvn -Dmaven.test.skip=true clean package java -Djarmode=tools -jar target/spring-petclinic-3.3.0-SNAPSHOT.jar extract java -Dspring.aot.enabled=true -XX:ArchiveClassesAtExit=./application.jsa -Dspring.context.exit=onRefresh -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar After the archive has been created, run the application with: java -Dspring.aot.enabled=true -XX:SharedArchiveFile=application.jsa -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar In my case, Spring Petclinic started in 1.5 seconds, reflecting 54% startup time reduction! Create the CDS archive using a container with CDS In the tutorial below, we will make use of Docker multi-stage builds to create a CDS archive of a Spring Boot application and use it in a Docker container. We will use a Liberica Runtime Container with CDS (tagged with a cds tag). Liberica Runtime Container is based on Liberica JDK, a Java runtime recommended by Spring, and Alpaquita Linux, a lightweight Linux distro tailor-made for Java. A natural choice for our experiment, I dare say. If you want to compare startup times, you can first build a standard container image using the following Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder RUN apk add wget WORKDIR /home/app ADD spring-petclinic-main /home/app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl WORKDIR /home/app COPY --from=builder /home/app/spring-petclinic-main/target/*.jar petclinic.jar CMD ["java", "-jar", "petclinic.jar"] Here, we create a fat jar and run it the usual way. Build the image with: docker build -t petclinic-standard . And then run it with: docker run petclinic-standard In my case, it took 5.3 seconds for the containerized application to start. Now, let’s make use of CDS and AOT. To create the archive in a Docker container, you need the following Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-21-cds-musl as builder WORKDIR /home/app ADD spring-petclinic /home/app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl as optimizer WORKDIR /app COPY --from=builder /home/app/spring-petclinic-main/target/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl ENTRYPOINT ["java", "-Dspring.aot.enabled=true", "-XX:SharedArchiveFile=application.jsa", "-jar", "/app/app.jar"] WORKDIR /app COPY --from=optimizer /app/extracted/dependencies/ ./ COPY --from=optimizer /app/extracted/spring-boot-loader/ ./ COPY --from=optimizer /app/extracted/snapshot-dependencies/ ./ COPY --from=optimizer /app/extracted/application/ ./ RUN java -Dspring.aot.enabled=true -XX:ArchiveClassesAtExit=./application.jsa -Dspring.context.exit=onRefresh -jar /app/app.jar Here, we have tree build stages, each of which uses the base image with Liberica JDK and CDS. During the first stage, we, we build the usual fat jar file. During the second stage, we make use of Spring Boot -Djarmode=tools to extract layers from our application. Running a layered jar in a containerized environment reduces overhead. In addition, the layering gives the developers more fine-grained control over the image updates. The most frequently updated layers are placed on top of the image, so in most cases, Docker only needs to update these top layers and pull other layers from the cache. During the third stage, we transfer extracted layers into a fresh image. We also run the application with the options required for creating a CDS archive. The entrypoint contains the -XX:SharedArchiveFile=application.jsa option for using the archive. Run the standard command for building a Docker image: docker build -t cdsimage . After that, you can run the container image of the application the usual way: docker run cdsimage |\ _,,,--,,_ /,`.-'`' ._ \-;;,_ _______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______ | | '---''(_/._)-'(_\_) | | | | | | | | | | _ | ___|_ _| | | | | |_| | | | __ _ _ | |_| | |___ | | | | | | | | | | \ \ \ \ | ___| ___| | | | _| |___| | _ | | _| \ \ \ \ | | | |___ | | | |_| | | | | | | |_ ) ) ) ) |___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / / ==================================================================/_/_/_/ :: Built with Spring Boot :: 3.4.2 2025-05-02T07:14:23.144Z INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting AOT-processed PetClinicApplication v3.4.0-SNAPSHOT using Java 21.0.7 with PID 1 (/app/app.jar started by root in /app) 2025-05-02T07:14:23.146Z INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : No active profile set, falling back to 1 default profile: "default" 2025-05-02T07:14:23.480Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2025-05-02T07:14:23.485Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2025-05-02T07:14:23.485Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.34] 2025-05-02T07:14:23.496Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2025-05-02T07:14:23.497Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 349 ms 2025-05-02T07:14:23.661Z INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2025-05-02T07:14:23.683Z INFO 1 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:3df44d9e-60d4-486d-9206-d135fc413101 user=SA 2025-05-02T07:14:23.684Z INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2025-05-02T07:14:23.741Z INFO 1 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2025-05-02T07:14:23.748Z INFO 1 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.6.5.Final 2025-05-02T07:14:23.753Z INFO 1 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2025-05-02T07:14:23.792Z INFO 1 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2025-05-02T07:14:23.802Z INFO 1 --- [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info: Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)'] Database driver: undefined/unknown Database version: 2.3.232 Autocommit mode: undefined/unknown Isolation level: undefined/unknown Minimum pool size: undefined/unknown Maximum pool size: undefined/unknown 2025-05-02T07:14:24.012Z INFO 1 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) 2025-05-02T07:14:24.013Z INFO 1 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2025-05-02T07:14:24.121Z INFO 1 --- [ main] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used. 2025-05-02T07:14:24.734Z INFO 1 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 14 endpoints beneath base path '/actuator' 2025-05-02T07:14:24.785Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' 2025-05-02T07:14:24.788Z INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 1.886 seconds (process running for 2.122) As a result, we achieved startup reduction by 60%! You can also run multiple containers based on this image. This way, they will share the same CDS archive, which may contribute to reduced memory consumption in the cloud. Use CDS with buildpacks Great news for Spring developers using buildpacks (and a good incentive to try them out for those who prefer traditional Dockerfiles): Paketo buildpacks support the creation of a CDS archive! Make sure that you have pack installed, and Paketo Base builder is the default builder (more on containerizing Java applications with buildpacks here). To make use of CDS and Spring AOT with buildpacks, run the following command: pack build petclinic-cds-pack --env BP_JVM_VERSION=21 --env BP_SPRING_AOT_ENABLED=true --env BP_JVM_CDS_ENABLED=true Alternatively, you can enable CDS and AOT directly in the Maven / Gradle plugin: org.springframework.boot spring-boot-maven-plugin true true process-aot plugins { java id("org.graalvm.buildtools.native") version "0.10.3" } tasks.named("bootBuildImage") { environment.putAll(mapOf( "BP_SPRING_AOT_ENABLED" to "true", "BP_JVM_CDS_ENABLED" to "true" )) } And then, build the container image with the usual command: mvn spring-boot:build-image for Maven or gradle bootBuildImage for Gradle. How to Use AOT Cache instead of AppCDS with Java 24 for even faster startup Java 24 came with JEP 483: Ahead-of-Time Class Loading & Linking, which is the first step towards integrating Project Leyden in the mainline OpenJDK. JEP 483 builds in AppCDS, but goes further and creates an AOT Cache where the classes are iniialized, loaded, and linked. This way, the JVM has even less work to do upon application start. Great news that Spring boot supports both CDS and AOT Cache. What is more, it is recommended to use AOT Cache instead of CDS if you have migrated to JDK 24. You can use AOT Cache with or without Spring AOT. Spring AOT enables more startup improvements, but it freezes Spring profiles, which may require extra attention from you. If, however, you want to use Spring AOT, make sure that you enabled the process-aot goal in the Maven/Gradle plugin. Enough talking, Let's see how we can use AOT Cache with Docker container images! You will need the following Dockerfile (remember that Spring AOT is optional): FROM bellsoft/liberica-runtime-container:jdk-24-stream-musl as builder WORKDIR /home/app ADD spring-petclinic-main /home/app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package FROM bellsoft/liberica-runtime-container:jdk-24-cds-slim-musl as optimizer WORKDIR /app COPY --from=builder /home/app/spring-petclinic-main/target/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted FROM bellsoft/liberica-runtime-container:jdk-24-cds-slim-musl WORKDIR /app ENTRYPOINT ["java", "-Dspring.aot.enabled=true", "-XX:AOTCache=app.aot", "-jar", "/app/app.jar"] COPY --from=optimizer /app/extracted/dependencies/ ./ COPY --from=optimizer /app/extracted/spring-boot-loader/ ./ COPY --from=optimizer /app/extracted/snapshot-dependencies/ ./ COPY --from=optimizer /app/extracted/application/ ./ RUN java -Dspring.aot.enabled=true -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -Dspring.context.exit=onRefresh -jar /app/app.jar RUN java -Dspring.aot.enabled=true -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -jar /app/app.jar Note the commands for using AOT Cache: RUN java -Dspring.aot.enabled=true -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -Dspring.context.exit=onRefresh -jar /app/app.jar RUN java -Dspring.aot.enabled=true -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -jar /app/app.jar With the first command, we perform the trial run and record the AOT configuration into the app.aotconf file. With the second command, we use this file to create the cache (app.aot). In the ENTRYPOINT, we specify the path to the cache file so that JVM could use it. Build the contaiiner image and run it: docker run --rm -p 8081:8081 petclinic-aot-cache |\ _,,,--,,_ /,`.-'`' ._ \-;;,_ _______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______ | | '---''(_/._)-'(_\_) | | | | | | | | | | _ | ___|_ _| | | | | |_| | | | __ _ _ | |_| | |___ | | | | | | | | | | \ \ \ \ | ___| ___| | | | _| |___| | _ | | _| \ \ \ \ | | | |___ | | | |_| | | | | | | |_ ) ) ) ) |___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / / ==================================================================/_/_/_/ :: Built with Spring Boot :: 3.4.2 2025-05-02T12:00:11.338Z INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting AOT-processed PetClinicApplication v3.4.0-SNAPSHOT using Java 24.0.1 with PID 1 (/app/app.jar started by root in /app) 2025-05-02T12:00:11.339Z INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : No active profile set, falling back to 1 default profile: "default" WARNING: A restricted method in java.lang.System has been called WARNING: java.lang.System::load has been called by org.apache.tomcat.jni.Library in an unnamed module (file:/app/lib/tomcat-embed-core-10.1.34.jar) WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module WARNING: Restricted methods will be blocked in a future release unless native access is enabled 2025-05-02T12:00:11.657Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2025-05-02T12:00:11.663Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2025-05-02T12:00:11.663Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.34] 2025-05-02T12:00:11.681Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2025-05-02T12:00:11.681Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 342 ms 2025-05-02T12:00:11.810Z INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2025-05-02T12:00:11.829Z INFO 1 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:c2ce2ed4-50c4-4182-8d9d-80102a5a5628 user=SA 2025-05-02T12:00:11.829Z INFO 1 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2025-05-02T12:00:11.881Z INFO 1 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2025-05-02T12:00:11.885Z INFO 1 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.6.5.Final 2025-05-02T12:00:11.886Z INFO 1 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2025-05-02T12:00:11.917Z INFO 1 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2025-05-02T12:00:11.921Z INFO 1 --- [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info: Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)'] Database driver: undefined/unknown Database version: 2.3.232 Autocommit mode: undefined/unknown Isolation level: undefined/unknown Minimum pool size: undefined/unknown Maximum pool size: undefined/unknown 2025-05-02T12:00:12.069Z INFO 1 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) 2025-05-02T12:00:12.069Z INFO 1 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2025-05-02T12:00:12.165Z INFO 1 --- [ main] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used. 2025-05-02T12:00:12.690Z INFO 1 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 14 endpoints beneath base path '/actuator' 2025-05-02T12:00:12.732Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' 2025-05-02T12:00:12.736Z INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 1.488 seconds (process running for 1.776) The application starts 67% faster! Summary We discussed three ways of using CDS with Spring Boot. You can: Create the archive and run the app on the local machine, Use a Dockerfile, Use a buildpack. To sum up, CDS is effortless to use and doesn’t require code refactoring. At the same time, it allows for a good startup improvement, up to 54%, depending on the configuration. This article analyzes only one application, so the results can vary depending on the program under evaluation. Another claimed advantage of CDS is reduced memory footprint when the archive is shared among multiple VM instances. If you’d like to measure footprint reduction, there’s a great article by Volker Simonis that gives detailed instructions on running the experiment, gathering the statistics, and evaluating the data. - [Microservices vs monoliths: which approach is better for your project?](https://bell-sw.com/blog/microservices-vs-monoliths-which-approach-is-better-for-your-project/): Microservices are still a buzzword despite the fact that they have been around for more than a decade. But curiously, the tables are turning, and microservices are increasingly mentioned in a rather unflattering context. This article is not going to ride the “microservices are evil” wave, nor will it be an anthem for microservices architecture with a “break up your monolith for exceptional scaling and resilience” tune. Instead, we will look into strengths and weaknesses of both approaches and discuss how to choose between them depending on your project, plans, and capacities. We will also discuss an alternative solution that could serve as an intermediate step between the two. Regardless of the approach, choosing a right base image is essential to keep cloud costs at bay. BellSoft develops Alpaquita containers for Java applications based on Liberica JDK and Alpine-compatible Alpaquita Linux that can help you save up to 30% RAM! Table of Contents What is a monolith? Pros and cons of monoliths What are microservices? Pros and cons of microservices Microservices or a monolith: how to choose? Modular applications: a structured monoliths ready for microservices What is a monolith? A monolithic application (or simply, a monolith) represents a single code base that includes all application layers: presentation, business logic, data access, etc. All these components are packed and deployed as a single unit. Monolithic architecture Monoliths used to be a traditional way of building applications, but this approach should not be considered old or worse, obsolete. Monoliths are suitable for small to medium-size applications that do not process large volumes of data. As user requests slash through a single ‘box’ right through to the database, the response times with monoliths are usually fast (and in the case of smaller data sets, even poor-quality SQL queries don’t affect the performance tangibly). The problems start when the application grows, new entities are added, new libraries are integrated, etc. Then the code turns into a tangled spider web, where a small change in one part of the system can lead to failures or unexpected behavior in a far corner of the application (the ripple effect). Spoiler alert: microservices do not magically solve the problem of bad code. In addition, scaling doesn’t come easy for monoliths, or rather, it doesn’t come cheap because you have to scale the whole application instead of several most critical parts. In broad terms, pros and cons of monolithic applications can be summarized as follows. Pros and cons of monoliths Advantages: Ease of development: Monoliths are easy to develop (at least, in the beginning, before introducing complex business logic), containerize, and deploy. Simple to debug and test: As monoliths function as a single system, they are a good fit for integration testing, and it is also much more convenient to debug them because all application components are tightly intertwined and right at your fingertips. Minimal latency: communication between application parts is seamless and fast, which contributes to shorter response times. Disadvantages: Not resilient: As all components are stuffed together in one tight box, the failure of one component may take down the whole application. Harder to scale: It is difficult to scale one part of a monolithic application without touching the rest, so you may either end up with performance bottlenecks or with huge cloud bills for scaling the whole app. Difficult to update: The ripple effect mentioned above makes it harder to introduce changes to the application without breaking or changing the existing components. What are microservices? Microservices architecture is an approach to building software as a set of independent services that can be deployed and maintained separately. Microservices communicate with each other over the network via language-agnostic protocols, so they can be written in different languages and use different technologies. Adding new features is easier than with monoliths because the addition of new services doesn’t break the normal operation of the existing ones. It also doesn’t necessitate the redesign of the wholly system, making release cycles shorter. Microservices architecture The true power of microservices lies in scaling. When particular services are overloaded, you scale them horizontally or vertically without touching the rest: this way, you maintain optimal performance of the application and pay for the cloud resources you actually use. But with great power comes great responsibility. Establishing the right connections between all microservices, whose numbers can reach hundreds or thousands, is no small feat. Testing the whole system with standard methods is nearly impossible. Plus you should do something with the common codebase: should you create a common library (and how do you update it) or keep the microservices stuffed with boilerplate? And that is only the tip of an iceberg. So if you don’t think the whole system through at the very beginning, you may end up with a monstrous mess of a system nobody understands or controls. Besides, a badly written monolith is one problem. Badly written microservices are the same problem multiplied by a number of services. So bad code broken down into microservices doesn’t turn into gold. It is still bad code, only you get even more problems specific to distributed systems. Pros and cons of microservices Advantages: Scalable and flexible: Individual services can be easily scaled horizontally to adapt to the current loads. In addition, it is easier to implement new features without impacting the existing codebase. Resilient: If one service goes down, others will continue functioning, allowing for high fault tolerance. Multi-language: The degree of service independence and isolation is so high that microservices can be written in various programming languages and using various frameworks and solutions, which enables the developers to use the advantages of multiple technologies for business benefit. Disadvantages: Hard to debug and test: While writing unit tests for individual services is nothing new, it is extremely difficult to test and debug the application on the whole. Additional latency: Services have to communicate with each other over the network, which leads to increased latency. Maintenance and development complexity: When writing microservices, developers have to decide which functions have to be separated, how to establish the connections between the services, how to update them in a unified way, etc. In addition, introducing changes is not as easy as it may sound, as teams have to be aware of the whole system layout, i.e., which service does what. Microservices or a monolith: how to choose? It’s tempting to throw in the usual “it depends.” But in reality, even if you lean towards one approach at the start, you may change your mind later. For instance, you have a small application and you are not planning to conquer the global market. However, somewhere along the way the business took a turn and started rapidly expanding. A monolith as it doesn’t meet user requirements any more, new functionality must be added. In addition, at some point, it may get flooded with requests, and your team is terribly overloaded trying to meet the SLAs. Separating several “hot” components from a monolithic application into separate services might be a good choice in this scenario. But don’t rush into the microservices jungle without considering the complexity this approach brings and whether you have the capacity to deal with it. Or rather: do the benefits of microservices architecture outweigh the complexity of maintaining them? After all, there are not so many giant enterprises where microservices will shine in all their glory. And considering that some of these giants such as Shopify do quite well with a monolith, and some of them like Uber migrate from microservices to macroservices admitting that microservice maintenance is too complicated, microservices don’t seem to be a “must-have” of a successful business. So choosing between microservices and a monolith boils down to counting and comparing pluses and minuses of both approaches for your use case, much like using the Descartes Square technique. Descartes Square to choose between microservices and a monolith Still can’t decide? In this case, there is an optimal middle ground — a modular application. Modular applications: a structured monoliths ready for microservices Modular architecture is the approach that focuses on dividing the application into smaller parts called modules, which are isolated from each other but have one codebase nevertheless. In other words, you still have a monolithic application, but its parts are not as intertwined as in a traditional monolith. Modules can be added or updated without affecting other components. In addition, communication between modules happens through API or events. With modular application, you leverage benefits of monoliths and microservices: Communication between modules is fast, Modules are isolated from each other, facilitating addition of new features, The application structure is clearly defined, Modules can be easily separated into services without running the whole app. As a result, modular applications can be considered a bridge between microservices and monoliths. So if you have a monolithic app, try breaking it down to modules instead of tearing it apart into microservices right away. If you develop Spring applications, there’s a great new project called Spring Modulith aimed at helping you build modular apps. And if at some point you decide that microservices will benefit your business more, you will have no problem dividing modules into services. - [What is Spring Modulith? Introduction to modular monoliths](https://bell-sw.com/blog/what-is-spring-modulith-introduction-to-modular-monoliths/): In our previous article, we compared monoliths to microservices and decided that a good intermediate solution would be to build a modular application. It is still a monolith with its advantages and drawbacks, but the code base is a lot more structured and prepared to be divided into microservices if the need arises. This article starts a series of hands-on tutorials on building robust modular applications with Spring Modulith, a Spring project aimed at helping the developers build, test, and maintain modular Spring Boot apps. By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can help you save up to 30% RAM! Table of Contents What is Spring Modulith How to set up application module structure Spring Modulith demo application: source code and key business logic Shipment module Customer module Calculator module Admin module What’s next: inter-module communication and verification with Spring Modulith What is Spring Modulith Spring Modulith is a relatively new Spring project that equips the developers with a toolkit for building modular Spring Boot applications. Spring Modulith doesn’t build the module structure for you. Instead, it provides opinionated guidance on how to arrange the code into loosely coupled modules within a single project. This guidance takes the form of warnings when running the tests aimed to verify the correct module structure. In addition, Spring Modulith provides support for module integration testing, observability, and asynchronous communication. How to set up application module structure Whether you are breaking up your legacy monolith or starting a modular application from scratch, you have to think the application structure through first. Modular monoliths are quite close to microservices, meaning that modules should be as independent as possible from each other and communicate through the API exposed to other modules. In a modular application, there are A main package, which contains the class used to run the application, Application modules, which are direct sub-packages of the main package. Subpackages contained in application modules are considered internal and should not be referenced by code from other modules (although it is possible to open them up, which we’ll demonstrate below). In addition, there should not be any circular dependencies, meaning that if module A references module B, module b cannot reference module A. That being said, let’s look at the possible module structure for a delivery service application we are going to work with in this series. The cornerstone modules of the application include: Shipment module: contains all information about shipment and handles shipment order processing, Customer module: serves as a gateway, providing a user interface for requesting a quote and placing the order, Calculator module: calculates the price of a shipment based on the input data and business logic, Admin module: performs administrative tasks and updates shipment delivery status. In addition, we have additional modules: DTO module: contains DTOs for all entities. These DTOs can be safely transferred between modules without violating the Spring Modulith rules. Global Exception module: includes a unified exception handler. The resulting workflow is as follows: A user sends a new shipment request through the customer module. The customer module asks the calculator API to validate the incoming price through the custom validator built within the customer module. The calculator returns the correct price to the customer module. If the price is correct, the customer module asks the shipment API to create a new order. If the calculated price doesn’t match the given price, the customer returns an error message to the. The shipment module creates the order. The ApplicationEventPublisher used in this module creates the event upon order creation. The customer module listens to the events from the shipment module. Upon receiving a notification about the order creation, it sends a message to the user through email or an SMS (in our case, it just logs the event for the sake of simplicity). The admin module also has a web interface accessible to administrators. The administrator can update the shipment delivery status through the admin module by calling the shipment API. The shipment module updates the order status and creates the event, which is listened to by the customer module. The processes described above can be depicted on the sequence diagram as follows. Sequence diagram for a modular delivery service app Spring Modulith demo application: source code and key business logic The code for the demo application we use is available on GitHub. The repository contains a fully-functional application that you can run on your machine. We tried to preserve the balance between a barebone demo application and a full-blown enterprise project, so some business logic is simplified for the sake of clarity, and some features usually absent in demo applications were added to shed light on scenarios you may encounter in a real-world project. The application is a Maven project based on Java 21 and Spring Boot 3.3, the latest version available at the moment of writing this article. Our Java runtime of choice was Liberica JDK recommended by Spring. You can download Liberica JDK 21 for your platform from the site or get it through IntelliJ IDEA. Spring Modulith dependencies include modulith-starter-core, modulith-starter-jpa (JDBC is also available), modulith-actuator, modulith-docs, modulith-observability, modulith-started-test, and modulith-bom: they all are provided in bulk when you choose the Spring Modulith dependency in Spring Initializr. We also added additional dependencies required for the project. The first is MapStruct, a library that allows for a convenient mapping of entities to DTO and vice versa. You can read more about integrating MapStruct into your Spring Boot project in this article. The second is libphonenumber, which is Google's library for parsing and validating phone numbers. You can browse the complete pom.xml here. The core structure of the project is depicted below. We have three classes in the main application package: one for running the application and two for creating application events, ShipmentCreateEvent and ShipmentStatusChangeEvent. We also have six modules: admin, calculator, customer, dto, globalexceptions, and shipment. src/main/java └── dev └── cat └── modular.monolith ├── ShipmentCreateEvent.java ├── ShipmentStatusChangeEvent.java ├── BeelineApplication.java ├── admin ├── calculator ├── customer └── dto └── globalexceptions └── shipment Shipment module The foundation of your project is the Shipment entity residing in shipment/model subpackage: @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Entity @Table(name = "shipment") public class Shipment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long customerId; private double weight; private String addressFrom; private String addressTo; private DeliveryStatus deliveryStatus; private double price; //equals and hashCode } The relation to customer is preserved in the form of customerId. This is not very JPA-ish, but we cannot have a nested entity from another module in this scenario. The addresses are saved as a String for simplicity's sake because preserving an address in the DB is an exciting, but different subject. DeliveryStatus, however, resides in the same package and can be safely referred to. The DTOs, ShipmentRequest and ShipmentResponse, go to the dto module: public record ShipmentResponse(Long id, Long customerId, double weight, String addressFrom, String addressTo, double price, String deliveryStatus) { } public record ShipmentRequest(@NotNull double weight, @NotBlank String addressFrom, @NotBlank String addressTo, @NotNull double price) { } The mappers for Shipment and DeliveryStatus go to the shipment/mapper subpackage: @Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE) public interface StatusMapper { String mapToStatusName(DeliveryStatus status); DeliveryStatus mapToStatus(String status); } @Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, uses = {StatusMapper.class}) public interface ShipmentMapper { ShipmentMapper INSTANCE = Mappers.getMapper(ShipmentMapper.class); Shipment mapToShipment(ShipmentRequest request); ShipmentResponse mapToShipmentResponse(Shipment shipment); } If you would like to know more about creating mappers with MapStruct, refer to this article. The repository class extends JpaRepository and is quite basic: public interface ShipmentRepository extends JpaRepository { List findByCustomerId(Long customerId); } Finally, the ShipmentService is responsible for all CRUD operations with the shipment: @Service @RequiredArgsConstructor public class ShipmentService { private final ShipmentRepository shipmentRepository; public ShipmentResponse createOrder(ShipmentRequest request, Long customerId) { Shipment shipment = ShipmentMapper.INSTANCE.mapToShipment(request); shipment.setCustomerId(customerId); shipment.setDeliveryStatus(DeliveryStatus.NEW); Shipment newShipment = shipmentRepository.save(shipment); return ShipmentMapper.INSTANCE.mapToShipmentResponse(newShipment); } public List findOrdersByCustomerId(Long id) { List shipments = shipmentRepository.findByCustomerId(id); return shipments.stream().map(ShipmentMapper.INSTANCE::mapToShipmentResponse).toList(); } public void updateShipmentStatus(Long id, String status) { Optional shipmentOpt = shipmentRepository.findById(id); if (shipmentOpt.isPresent()) { Shipment shipment = shipmentOpt.get(); shipment.setDeliveryStatus(DeliveryStatus.valueOf(status)); shipmentRepository.save(shipment); } else throw new EntityNotFoundException("Couldn't find shipment with id " + id); } } Note that we will enhance this class in the next article. Customer module The Customer entity is also simple: @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Entity @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; private String phoneNumber; private String email; private String country; //equals and hashCode } There are also record classes for CustomerResponse and CustomerRequest in the dto module: public record CustomerRequest(@NotBlank String firstName, @NotBlank String lastName, @NotBlank String country, @NotBlank String phoneNumber, @NotBlank @Email String email) { } public record CustomerResponse(Long id, String firstName, String lastName, String country, String phoneNumber, String email) { } The corresponding mapper resides in the customer/mapper subpackage. CustomerService is responsible for CRUD operations with the Customer entity (similarly to ShipmentService, we will enhance it later): @Service @RequiredArgsConstructor @Slf4j public class CustomerService { private final CustomerRepository repository; public CustomerResponse saveCustomer(CustomerRequest request) { Customer customer = CustomerMapper.INSTANCE.mapToCustomer(request); return CustomerMapper.INSTANCE.mapToCustomerResponse(repository.save(customer)); } public CustomerResponse findCustomerById(Long id) { Optional customerOpt = repository.findById(id); if (customerOpt.isPresent()) { return CustomerMapper.INSTANCE.mapToCustomerResponse(customerOpt.get()); } else throw new EntityNotFoundException("Couldn't find customer with id " + id); } } Also, as the customer module serves as a gateway, it has a CustomerController: @RestController @RequestMapping("/api") @RequiredArgsConstructor @Validated public class CustomerController { private final CustomerService service; @PostMapping("/customers") public ResponseEntity createCustomer( @NotNull @Valid @RequestBody @CorrectPhoneNumber @UniquePhoneNumber @UniqueEmail CustomerRequest request) { return ResponseEntity.ofNullable(service.saveCustomer(request)); } } Right now, it’s almost empty, but don’t worry, we will implement the required method in the next article, the Spring Modulith style. Note the peculiar @CorrectPhoneNumber, @UniquePhoneNumber,etc. annotations. These are our custom-made validators aimed at verifying application-specific conditions. In our case, the check whether the provided phone number is valid and absent in the database; the email is also checked on uniqueness. Custom validators in Spring Boot are powerful tools that deserve a separate article, which is already in the makings! Calculator module As for the calculator, we have a DTO in the dto package: public record CalculatorRequest(@NotBlank double weight, @NotBlank String addressFrom, @NotBlank String addressTo) { } And a single @Service class for calculating the shipment price: @Service public class PriceCalculator { public double calculatePrice(CalculatorRequest request) { //that's pure placeholder for the simplicity sake :) return 10.0 * request.weight(); } } Note that we didn’t implement any heavyweight logic for calculating the shipment price as this is not the goal of this article. Admin module The admin module contains a single @RestController class for the administrator to work with the application: @RestController @RequestMapping("/api/admin") @RequiredArgsConstructor @Validated public class AdminController { } What’s next: inter-module communication and verification with Spring Modulith What we explained here is a carcass that has nothing to do with Spring Modulith yet. It could be another standard monolithic application, albeit with a different structure. However, Spring Modulith will fill our plain project with bright colors! Follow this link to know how to Verify application module structure, Establish communication between modules via external APIs, Set up asynchronous communication, Document application modules. See you there! - [How to build a modular application with Spring Modulith](https://bell-sw.com/blog/how-to-build-a-modular-application-with-spring-modulith/): In the previous article, we discussed how to prepare for building a modular monolith with Spring Modulith by setting up the right application module structure. We also summarized the key business logic of a demo Spring Modulith application we use in this tutorial series and set up a module verification test. The application code is available on Github. A quick reminder: this is a basic, yet fully-functioning delivery service application based on Java 21 and Spring boot 3.3 and integrating Spring Modulith. In this tutorial, we will: Establish synchronous communication between modules via APIs, Set up asynchronous communication supported out-of-the-box by Spring Modulith, Discuss how to use internal module subpackages without violating Spring Modulith constraints, Generate documentation for application modules. Sounds like a Spring Modulith crash course? In that case, buckle up, we’re setting off! Table of Contents Application module structure: summary Set up module verification with Spring Modulith Set up external APIs for modules Expose necessary packages with Named Interfaces Configure asynchronous modules communication Document application modules Coming up: testing application modules Application module structure: summary For those of you who have just joined us: we are building a modular monolith with the following structure: src/main/java └── dev └── cat └── modular.monolith ├── ShipmentCreateEvent.java ├── ShipmentStatusChangeEvent.java ├── BeelineApplication.java ├── admin ├── calculator ├── customer ├── dto ├── globalexceptions └── shipment Here, we have four core modules: shipment for processing shipment data, customer for providing a gateway and handling customer data, calculator for calculating the shipment price, admin for updating the order status. We also have two auxiliary modules for handling exceptions (globalexceptions) and DTOs (dto). In addition, main application package contains three classes: BeelineApplication is the main class annotated with @SpringBootApplication, ShipmentCreateEvent and ShipmentStatusChangeEvent are record classes responsible for creating application events (we will discuss them in detail below). Set up module verification with Spring Modulith If you haven’t studied or experimented with Spring Modulith before, you may be wondering: how exactly does it help with building a modular application? Spring Modulith creates an application module model and analyzes the dependencies between them. ApplicationModules can be pointed to the main application class to derive a module structure. This structure can be used in a JUnit test to analyze the interconnection between modules: public class SpringModulithTests { ApplicationModules modules = ApplicationModules.of(BeelineApplication.class); @Test void shouldBeCompliant() { modules.verify(); } If any of the Spring Modulith rules are violated, the test will fail, and we will see a detailed error message, which will allow us to set the dependencies straight. Set up external APIs for modules The modules that we described in the previous article exist in a vacuum. But now, the fun part begins: we need to establish communication between them. In a traditional monolith, we could simply inject a necessary bean wherever we need in our business logic. Even a modular application without Spring Modulith would allow for that, especially considering that all classes in module subpackages are public. However, this is what we aim to get away from. We want to preserve module independence, leaving only one communication channel in the form of API. For instance, let’s try to inject the PriceCalculator bean directly into CustomerController like that: @RestController @RequestMapping("/api") @RequiredArgsConstructor @Validated public class CustomerController { private final PriceCalculator calculator; @PostMapping("/quote") public ResponseEntity calculatePrice( @NotNull @Valid @RequestBody CalculatorRequest request) { Double price = calculator.calculatePrice(request); return new ResponseEntity<>(price, HttpStatus.OK); } } Now, if we run the module verification test, we will see a nasty error message similar to the one below: org.springframework.modulith.core.Violations: - Module 'customer' depends on non-exposed type dev.cat.modular.monolith.calculator.service.PriceCalculator within module 'calculator'! CustomerController declares constructor CustomerController(PriceCalculator) in (CustomerController.java:0) - Module 'customer' depends on non-exposed type dev.cat.modular.monolith.calculator.service.PriceCalculator within module 'calculator'! Method calls method in (CustomerController.java:68) The test fails because we violated the Spring Modulith rule and injected the non-exposed dependency. How do we establish the communication between modules comme il faut? We don’t want to directly expose the @Service classes by placing them into the base module package, so the best solution is to create an external API interface that will serve as a bridge between modules’ inner kitchen and other stakeholders. Let’s start with the calculator module. We have a CalculatorAPI interface in the base module package with only one method: public interface CalculatorAPI { double calculatePrice(CalculatorRequest request); } The PriceCalculator class needs to implement this interface: @Service public class PriceCalculator implements CalculatorAPI { @Override public double calculatePrice(CalculatorRequest request) { //that's pure placeholder for the simplicity sake :) return 10.0 * request.weight(); } } Now, instead of referencing the PriceCalculator service class, our CustomerController will depend on CalculatorAPI, which is open to other modules due to its location in the base package: @RestController @RequestMapping("/api") @RequiredArgsConstructor @Validated public class CustomerController { private final CalculatorAPI calculatorAPI; private final CustomerService service; @PostMapping("/quote") public ResponseEntity calculatePrice( @NotNull @Valid @RequestBody CalculatorRequest request) { Double price = calculatorAPI.calculatePrice(request); return new ResponseEntity<>(price, HttpStatus.OK); } } We can also inject CalculatorAPI into our custom price validator, which resides in the customer/validation/price package: @Constraint(validatedBy = PriceConstraintValidator.class) @Target( { ElementType.PARAMETER, ElementType.FIELD } ) @Retention(RetentionPolicy.RUNTIME) public @interface CorrectShipmentPrice { String message() default "Shipment price doesn't match the requested price."; Class[] groups() default {}; Class[] payload() default {}; } @RequiredArgsConstructor public class PriceConstraintValidator implements ConstraintValidator { private final CalculatorAPI calculatorAPI; @Override public boolean isValid(ShipmentRequest request, ConstraintValidatorContext constraintValidatorContext) { double price; if (request == null) return true; price = calculatorAPI.calculatePrice( new CalculatorRequest(request.weight(), request.addressFrom(), request.addressTo())); return request.price() == price; } } The interfaces for other modules are created in a similar fashion. The ShipmentAPI interface: public interface ShipmentAPI { ShipmentResponse createOrder(ShipmentRequest request, Long customerId); void updateShipmentStatus(Long id, String status); List findOrdersByCustomerId(Long id); } The CustomerAPI interface: public interface CustomerAPI { CustomerResponse findCustomerById(Long id); } ShipmentService and CustomerService should implement ShipmentAPI and CustomerAPI, respectively. The admin module won’t expose an API because in our case, no module depends on it. We can now inject ShipmentAPI into CustomerController and write a method for dealing with shipments: @RestController @RequestMapping("/api") @RequiredArgsConstructor @Validated public class CustomerController { private final ShipmentAPI shipmentAPI; private final CalculatorAPI calculatorAPI; private final CustomerService service; @PostMapping("/customers") public ResponseEntity createCustomer( @NotNull @Valid @RequestBody @CorrectPhoneNumber @UniquePhoneNumber @UniqueEmail CustomerRequest request) { return ResponseEntity.ofNullable(service.saveCustomer(request)); } @GetMapping("/customers/{id}/shipments") public List getAllOrdersForCustomer( @NotNull @PathVariable @ExistingCustomer long id) { return shipmentAPI.findOrdersByCustomerId(id); } @PostMapping("/quote") public ResponseEntity calculatePrice( @NotNull @Valid @RequestBody CalculatorRequest request) { Double price = calculatorAPI.calculatePrice(request); return new ResponseEntity<>(price, HttpStatus.OK); } @PostMapping("/customers/{id}/shipment") public ResponseEntity createShipmentOrder( @NotNull @Valid @RequestBody @CorrectShipmentPrice @UniqueAddress ShipmentRequest request, @NotNull @PathVariable @ExistingCustomer long id ) { return ResponseEntity.ofNullable(shipmentAPI.createOrder(request, id)); } } Let’s also inject CustomerAPI and ShipmentAPI into AdminController for the administrator to be able to change the shipment status and browse data on customers: @RestController @RequestMapping("/api/admin") @RequiredArgsConstructor @Validated public class AdminController { private final ShipmentAPI shipmentAPI; private final CustomerAPI customerAPI; @PostMapping("/shipments/{id}") @ResponseStatus(HttpStatus.OK) public void updateShipmentStatus( @NotNull @PathVariable long id, @NotNull @RequestBody String status) { shipmentAPI.updateShipmentStatus(id, status); } @GetMapping("/customers/{id}") @ResponseStatus(HttpStatus.OK) public ResponseEntity getCustomerById( @NotNull @PathVariable long id) { return ResponseEntity.ofNullable(customerAPI.findCustomerById(id)); } } Expose necessary packages with Named Interfaces So far, we managed to establish communication between modules via API without opening up private module packages. But remember that we have several DTOs in the dto module. We could place them into the base module package, so that other modules can freely refer to them, but this is not a very graceful solution. It would be better to keep the DTOs organized in subpackages. But if you simply create the calculator, shipment, and customer subpackages in the dto module and place the corresponding DTOs there, the verification tests should fail with the following message: org.springframework.modulith.core.Violations: - Module 'calculator' depends on non-exposed type dev.cat.modular.monolith.dto.calculator.CalculatorRequest within module 'dto'! CalculatorRequest declares parameter CalculatorRequest.calculatePrice(CalculatorRequest) in (CalculatorAPI.java:0) - Module 'calculator' depends on non-exposed type dev.cat.modular.monolith.dto.calculator.CalculatorRequest within module 'dto'! Method has parameter of type in (CalculatorAPI.java:0) - Module 'shipment' depends on non-exposed type dev.cat.modular.monolith.dto.shipment.ShipmentResponse within module 'dto'! ShipmentResponse declares return type ShipmentResponse.createOrder(ShipmentRequest, Long) in (ShipmentAPI.java:0) ... How do we solve this problem? We can make private module subpackages accessible to other modules with the help of the @NamedInterface annotation. For that purpose, we placed package-info.java files in each of the dto subpackages: dto └── calculator ├── CalculatorRequest.java └── package-info.java └── customer ├── CustomerRequest.java ├── CustomerResponse.java └── package-info.java └── shipment ├── package-info.java ├── ShipmentRequest.java └── ShipmentResponse.java These files are annotated accordingly. package-info.java in dto/calculator: @org.springframework.modulith.NamedInterface("dto-calculator") package dev.cat.modular.monolith.dto.calculator; package-info.java in dto/customer: @org.springframework.modulith.NamedInterface("dto-customer") package dev.cat.modular.monolith.dto.customer; package-info.java in dto/shipment: @org.springframework.modulith.NamedInterface("dto-shipment") package dev.cat.modular.monolith.dto.shipment; Now, other modules are allowed to refer to these named interfaces, and the Modulith verification tests should pass without issues. Configure asynchronous modules communication Up until now, the communication between our modules was strictly synchronous. However, we can also implement asynchronous communication with the help of the ApplicationEventPublisher interface provided by the Spring Framework. As a result, certain actors will be able to emit events upon certain actions, and listeners from other modules (one or however many) can process these events. Our application has two types of events: ShipmentCreateEvent, created when a new shipment is saved to the database, and ShipmentStatusChangeEvent for when the status of a shipment changes. The events themselves are record classes located in the main application package. They contain nothing but an id: public record ShipmentCreateEvent(Long orderId) { } public record ShipmentStatusChangeEvent(Long orderId) { } As these events are emitted upon some changes performed to the shipment, they should be generated within the shipment module, in the ShipmentService class: @Service @RequiredArgsConstructor public class ShipmentService implements ShipmentAPI { private final ShipmentRepository shipmentRepository; private final ApplicationEventPublisher events; @Override @Transactional public ShipmentResponse createOrder(ShipmentRequest request, Long customerId) { Shipment shipment = ShipmentMapper.INSTANCE.mapToShipment(request); shipment.setCustomerId(customerId); shipment.setDeliveryStatus(DeliveryStatus.NEW); Shipment newShipment = shipmentRepository.save(shipment); events.publishEvent(new ShipmentCreateEvent(newShipment.getId())); return ShipmentMapper.INSTANCE.mapToShipmentResponse(newShipment); } @Override public List findOrdersByCustomerId(Long id) { List shipments = shipmentRepository.findByCustomerId(id); return shipments.stream().map(ShipmentMapper.INSTANCE::mapToShipmentResponse).toList(); } @Override @Transactional public void updateShipmentStatus(Long id, String status) { Optional shipmentOpt = shipmentRepository.findById(id); if (shipmentOpt.isPresent()) { Shipment shipment = shipmentOpt.get(); shipment.setDeliveryStatus(DeliveryStatus.valueOf(status)); shipmentRepository.save(shipment); events.publishEvent(new ShipmentStatusChangeEvent(id)); } else throw new EntityNotFoundException("Couldn't find shipment with id " + id); } } Here, we injected the ApplicationEventPublisher bean, which is responsible for publishing the events. We also updated the createOrder and updateShipment methods, adding the event publication functionality. The newly created events contain the shipment id. Note that we avoid interaction with other application modules completely: the ApplicationEventPublisher doesn't care who or how many event listeners there are, thus enabling efficient module decoupling. According to the business logic, the customer module listens to the shipment-related events and forwards the information to the user. This task is laid upon CustomerService: @ApplicationModuleListener void onUpdateShipmentStatusEvent(ShipmentStatusChangeEvent event) { log.info("Changed status of shipment: {}", event.orderId()); } @ApplicationModuleListener void onShipmentCreateEvent(ShipmentCreateEvent event) { log.info("Created shipment: {}", event.orderId()); } Note the @ApplicationModuleListener annotation provided by Spring Modulith: it serves as a shortcut to integrating application modules via events. Document application modules Spring Modulith provides a convenient way to generate documentation for the modular app in the form of C4 or UML diagrams or Application Module Canvas. We can use the modules generated earlier with ApplicationModules and pass them into the Documenter class: @Test void writeDocumentationSnippets() { new Documenter(modules) .writeModulesAsPlantUml() .writeIndividualModulesAsPlantUml(); } Here, we create the UML diagram of the whole module system and each module separately. The diagrams will be generated in the target/spring-modulith-docs directory. This is how the general diagram look like as a result: Module structure diagram Coming up: testing application modules Excellent! We hope that our explanations will help you to build a robust modular application using the power of Spring Modulith. But Spring Modulith has a lot more in stock: it exposes application structure as actuator endpoints, provides observability support, and supports module integration testing. So if you would like to read more articles dedicated to Spring Modulith or other mighty Spring capabilities, subscribe to our newsletter and be the first to know about them! - [Four perspectives on code review inefficiencies](https://bell-sw.com/blog/four-perspectives-on-code-review-inefficiencies/): If we had half-assembled tangible products piling up and overloading our store, we’d be alarmed. If we had staff fighting in the store, we’d be alarmed, too. If students at school were not progressing in their skills, we’d be apprehensive. Yet, whenever I ask people, “Why are you using code reviews?” the answer is usually: We’ve been doing this for ages, and it’s been working just fine. Human beings have been manipulating information since the dawn of civilization, but IT as an industry is very young. The term “Information Technology” was only coined in 1958. IT is also quite complex to reason about: we work with information, and it’s intangible. We cannot physically see queues of half-done tasks obstructing our view and slowing everything down, we don’t physically fight over conflicts in reviews, we can’t assess how well we learn from reading code, and it’s very hard to rationally evaluate the quality of tasks to see how certain practices improve it. I’m Vitaly Sharovatov, Developer Advocate for Qase, an all-in-one QA and test management platform. I’m a quality enthusiast with more than 20 years of experience and a passion for helping people build great products. In this article, I want to present four perspectives on classical pull-request-based asynchronous code review, which I believe explain why this practice isn’t as efficient as most people think. Table of Contents Psychological perspective Educational / knowledge sharing perspective Quality Perspective Economical perspective Alternatives to code reviews References Psychological perspective I have a hobby, leathercraft. I’m a novice, and I know I have years of study and practice ahead of me. I am learning it alone by watching YouTube and reading articles. Every single task I approach, I do it the way I know it should be done. If I show my crafted wallet to someone, and that person criticizes it a lot and gives hundreds of comments like “you should have thought of skiving the edges more,” I would perceive this criticism badly, even knowing that the person might be right. Work done well fulfills our need for competence. Every human being associates the results of their work with themselves, and if I’m told my wallet is bad, I’m still going to be hurt. Valuing something we put effort into is innate to our nature. The more effort a person puts into a task, the more vulnerable they become to criticism, especially if the criticism feels unfair. Fairness is key here. If I were studying leathercraft with a teacher who showed me how to do it right and then made mistakes while working on it, I would perceive their criticism as fair: we synchronized the “how,” the approach, and the methods before the work. With traditional code reviews, there’s no synchronization before the work. The person does the task how they think is best, passes it for review, and receives criticism. In many cases, this triggers a feeling of unfairness, which, in turn, triggers negativity and sometimes conflicts or irritation. The internet is full of posts and articles on “solving” this negativity problem. Most of these posts talk about providing more positive feedback or wrapping criticism between two layers of positive feedback, known as the “sandwich technique.” However, studies have shown that these approaches do not yield good results, and candid criticism is more beneficial. This creates a vicious circle: negative feedback is perceived badly, while positive feedback doesn’t highlight problems and is essentially useless. The root cause of this problem is that code review is done without prior synchronization on how the task is to be approached. This problem disappears completely with pair programming, where two people work on a task together, synchronizing constantly. If I were crafting my leather wallet with a colleague, there would be no need for a “code review” after the work is done, as we both would see and correct issues while working together. The problem would also be less significant if two or more engineers agreed on how the work was to be done before the actual work began. This could be implemented as a pair (or ensemble/mob) session to create a technical plan for feature implementation, after which the developer would work on the feature alone. In this case, even if a developer encounters some issues, they will be smaller and will not require significant refactoring. Educational / knowledge sharing perspective Sometimes, code reviews are said to have a knowledge-sharing goal. The idea is that the author of the code will learn something from the reviewer’s comments, and the reviewer will learn something from the code under review. From the educational sciences perspective, there are a few issues with using code reviews to learn and share knowledge. For the reviewer, reading code does not lead to much knowledge acquisition. The reason is simple: we learn much more through practice. If a reviewer were to interact with the code while reading — breaking, refactoring, running, testing, and debugging — they would learn much more than from just reading it. Additionally, a reviewer only sees the final solution, not the problem analysis and solution synthesis process. These two aspects are the most important parts of feature development, while the code is just a by-product. For the author of the code, the learning process is even less efficient, as all they get after the code review are some comments. While comments are feedback that can be valuable, with code reviews, this feedback is too late from an educational perspective. Traditional education follows this process: The teacher gives all the necessary guidance and information. The student practices to learn it. The teacher checks the results of the practice and potentially highlights issues. In code review, the first stage is absent: no one gives the author any guidance or information, so the author practices without knowing if they are learning correctly or incorrectly. The reviewer does not synchronize their understanding of “how” with the reviewee before the task is done. As a result, the practice step only reinforces existing knowledge and does not lead to new knowledge acquisition. The author might rework the code upon receiving comments, but this is simply inefficient. Modern educational systems focus on group learning, where students learn and practice something new together with the teacher. With this approach, feedback is instant, and students never practice and learn something they will have to unlearn and redo later. This approach is very similar to pair or ensemble programming, where two or more employees solve problems and write code together, learning from each other all the time. Quality Perspective Most companies believe quality gates to be efficient measures against bugs. When presented with a pull request for review, a reviewer usually has two options: Study the requirements or user story and the problem behind it. Come up with a solution. Write the code for the solution. Compare this solution with what the author coded. Simply read the code. Option one could potentially allow a decent level of quality control because the author and the reviewer would be comparing two solutions to the same problem based on their optimality. However, this approach doubles the Lead Time for the feature, as the reviewer has to do the same work the author did, but asynchronously. Option two takes much less time and, as studies show, is very ineffective. Most of the issues found are either “taste”-based and aren’t defects or could be detected using linters or more powerful static analysis tools like Qodana. You can check how many actual defects your code review process finds. And if it finds many, ask yourself: should the quality assurance procedures be shifted left a bit? Would pair or ensemble programming allow for avoiding issues altogether before the code is completed while also yielding a more optimal solution to the problem? Economical perspective Most companies aim to be faster — faster development, faster testing, and faster deployment. They want clients to get new features as soon as possible. Many managers are already aware of the cost of delay, which is the economic impact of a delay in project delivery. Additionally, most companies do not want to sacrifice quality while aiming to be really fast. As noted above in the Quality Perspective section, code reviews rarely yield significant quality improvement and come at the cost of a substantial slow-down in the workflow. In many companies, code reviews can take from 20 to 40% of the total lead time. The graph below is just an illustration. I suggest you run your own analysis to see how much you usually pay in the cost of delay for doing code reviews. I have yet to find a team where pair programming wasn’t more efficient and effective than solo work plus code reviews. Code review vs pair programming Alternatives to code reviews As you might have noticed, in all perspectives I compared code reviews with pair programming — a collaborative practice where two engineers work together on one machine on a single task. Pair programming prevents all the inefficiencies associated with pull-request-based code reviews. It increases the quality of the code written, significantly improves knowledge sharing, and speeds up the overall development process. The only approach found to be even more efficient than pairing is ensemble (mob) programming. However, both pair and ensemble (mob) programming require a certain effort to start. Both practices demand a significant change of habits for engineers and managers. Many engineers, particularly experienced ones, resist collaborative practices mostly because they have never tried them. Managers, on the other hand, sometimes still think in terms of “why pay twice for the same feature,” failing to understand the economics of software development. Changing habits is hard, and changing beliefs is even harder. A good way to improve the existing culture and code reviews is to start development in a pair. During this stage, two engineers decide on the architecture and technical plan for the feature or user story. Once the plan is composed, the pair can disassemble and proceed working individually. Code review vs pair research vs pair programming This approach allows the code review practice to remain intact after development, so those engineers and managers who insist on it will not have to change their beliefs. Additionally, this approach significantly speeds up the overall development, almost reaching the speed of pair programming, because the hardest part — research — is done in a pair. Two engineers do the research and come up with an artifact — a technical plan — which is the result of their synchronization on “how” the feature is to be developed. This dramatically reduces the probability of rework later. The collaborative research and synchronization enable better knowledge sharing than any code review: during the pair research, both engineers will remember the problem they are solving, the solutions they considered, and which one they agreed to implement. Additionally, the code review stage in this scenario will be much shorter and will not trigger conflicts. The time required is smaller simply because a second engineer must only confirm that the feature was developed according to the plan they both created. Conflicts do not arise for the same reason: there is nothing to argue about — the plan is either followed or not. What’s most beautiful about this “pair research” approach is that it’s simple, so most engineers do not object to it, and most managers do not see it as “overpaying.” I hope this article provoked you to re-think the code-review practice and maybe implement at least a “pair research” approach at work. References Chapter 11 - Factors Critical to the Success of Knowledge Management The Knowledge-Creating Company A review of software Inspections, Porter, Siy, Votta, 1996 Expectations, Outcomes, and Challenges of Modern Code Review, 2013 Investigating technical and non-technical factors influencing modern code review, 2015 Information Needs in Contemporary Code Review, Pascarella, Spadini, Palomba, Bruntink, Bachelli, 2018 The Cost of Interrupted Work: More Speed and Stress, Mark, Gudith, Klocke, 2008 The myth of multitasking, Nass, 2013 Code Reviews Do Not Find Bugs. How the Current Code Review Best Practice Slows Us Down Modern code review: a case study at google, 2018 Reconfiguration of task-set: Is it easier to switch to the weaker task? Psychological Research, Monsell, S., Yeung, N., & Azuma, R. (2000) Multitasking: Switching costs (american psychological association) Executive Control of Cognitive Processes in Task Switching — Joshua S. Rubinstein, David E. Meyer, Jeffrey E. Evans The correlation between organizational culture and knowledge conversion on corporate performance A Meta-Analysis of Ten Learning Techniques The Costs and Benefits of Pair Programming The effectiveness of pair programming: A meta-analysis Evaluating Effectiveness of Pair Programming as a Teaching Tool in Programming Courses Increasing Quality with Pair Programming - An Investigation of Using Pair Programming as a Quality Tool The Case for Collaborative Programming Strengthening the Case for Pair Programming Mob vs Pair: Comparing the two programming practices – a case study Mob Programming: A Qualitative Study from the Perspective of a Development Team Leveraging the Mob Mentality: An Experience Report on Mob Programming - [How to increase the performance of Java applications](https://bell-sw.com/blog/how-to-increase-the-performance-of-java-applications/): The heading of this article could very well be the title of a book or even a book series. So, this blog post aims not to boil the ocean but to equip you with the necessary gear to navigate this ocean safely. Below, you will find three sections dedicated to four key performance indicators: memory consumption, startup time, throughput, and latency. Each section provides an overview of recommendations for improving each indicator and links to additional resources for a deeper dive into the topic. Table of Contents How to reduce the startup of Java applications How to optimize Java memory consumption How to improve throughput and latency of Java applications How to reduce the startup of Java applications Slow application startup is irritating per se, but the problem worsens when you restart your services multiple times a day and use cloud services that charge you for compute time. So, for some enterprises, startup time is more critical than other performance indicators. Java applications are great marathon runners, meaning that they demonstrate good overall performance in the long term. However, they may take several minutes to warm up, i.e., reach stable peak performance. During this period, they process fewer requests and consume more memory, which leads to higher latency at the start and memory overutilization. Different stages of Java start and their impact on overall startup time are described in detail in this article. Luckily, several approaches tackle the issue of slow Java startup. We wrote several articles dedicated to each of them so you can take a closer look at each solution and choose one that fits your project: Class Data Sharing (CDS) is a JVM feature that provides good startup improvement without code refactoring. The article How to use CDS with Spring Boot explains how CDS works and to what extent it may influence startup time. Java startup can be reduced to several milliseconds thanks to the Ahead-of-time (AOT) compilation provided by GraalVM Native Image. The main downside of this solution is incompatibility with Java’s dynamic features. It means that you may have to rewrite the code or provide another workaround to befriend Native Image with your app. Follow this guide to using Native Image with Java and solving potential incompatibility issues to get a taste of this technology. Another solution that slashes Java startup from minutes to milliseconds is Coordinated Restore at Checkpoint (CRaC). This OpenJDK project enables developers to save a paused Java app to a file and restore it from the file much like a video game. Learn more about operating principles, benefits, and potential pitfalls in the article A guide to CRaC Project. One more technology aimed at reducing Java startup time is Project Leyden, currently under development. Project Leyden will rely on CDS and Ahead-of-time compilation to static run-time images. Early access builds of Project Leyden are already available, but not intended to be used in production. How to optimize Java memory consumption If you don’t set the memory limits of your Java application, you risk turning it into a glutton that will eat away all available resources and, eventually, your cloud budget. To prevent this from happening or solve the issue if you have already encountered it, follow the tried-and-true recommendations and best practices gathered in the guides below: Containerized Java applications tend to get bloated, often containing unnecessary packages or image layers. Find out how to help your Java image lose weight by following a few easy steps from the guide on How to reduce the size of Docker container images. Choosing the right base Java image may help you reduce image size twice. Find out how to dockerize a Java application with the smallest base image. Adjusting JVM memory limits is an arduous, trial-and-error task. As each use case is unique, writing a guide on how to do that is impossible. However, having a list of key JVM memory configuration options with brief descriptions may be handy. Switching to distroless images may be tempting as this technology promises to minimize the footprint of your containerized apps and increase security as a bonus (no Linux distribution, no problems, so to speak). But are there any benefits for Java applications? Read this question-and-answer post on Distroless containers for Java to find out. Even seasoned developers can make mistakes when working with containers, which may result in excessive memory consumption and other performance-related issues. Forewarned is forearmed: learn about common containerization pitfalls and ways to to avoid them. How to improve throughput and latency of Java applications Numerous factors may affect the latency and throughput of your Java application, from architecture to database interaction or garbage collector implementation. We prepared several guides that will help you get started with identifying and eliminating possible performance issues without diving into fine-tuning: A clear picture of code hot spots and performance bottlenecks in your project is necessary for efficient performance improvement. So, you can start with gathering and evaluating application metrics with tried-and-true Java Flight Recorder and Mission Control. The Java platform offers a variety of Garbage Collectors tailored to specific purposes and environments. Sometimes, selecting the right GC may help tangibly improve performance without additional adjustments. To find out more about GC mechanisms in Java and available GC implementations, refer to the Guide to Java Garbage Collection. The Java Virtual Machine (JVM) implementation may also affect your application's performance. For instance, OpenJ9 is claimed to be more performant than HotSpot. We decided to study HotSpot and OpenJ9 to verify their performance. Check out the HotSpot vs. OpenJ9 study to learn the results. Newer Java versions are associated with better overall performance than the legacy ones due to new features and numerous optimizations introduced to the compiler and garbage collections. So, the obvious advice would be to migrate from Java 8 or 11 to Java 17, but many enterprises need more time to complete the migration due to the complexities of the procedure. However, it is possible to bring the performance of newer Java versions to JDK 8 or 11-based projects without upgrading the Java version. Find out how to boost the performance of your application without significant code changes with Liberica JDK Performance Edition 8 and 11. If you would like to read more articles dedicated to Java performance and guides on using cutting-edge tools with your Java app, subscribe to our newsletter. We will send you a monthly digest of our latest posts! - [How to use Project Leyden with Spring Boot](https://bell-sw.com/blog/how-to-use-project-leyden-with-spring-boot/): Project Leyden Early Access builds are already available, so the time for experiments has come! You can follow the instructions in the README.md of the Project’s release notes and run the benchmark provided by the team. But I was also curious to see Leyden in action with Spring Boot. Let’s couple Project Leyden with our favorite framework and see what startup gains we can already achieve. IMPORTANT NOTE: Project Leyden EA builds are based on experimental code and are not meant for production use. In addition, some features in the EA builds may be changed or removed, and the workflow may also change. So, this article will be updated accordingly in the future. By the way, did you know that you can reduce the RAM consumption of your Spring Boot services by up to 30%? Discover Alpaquita Containers tailor-made for Spring Boot. Buidpacks are also available! Table of Contents What is Project Leyden? Use Project Leyden with Spring Boot Looking into the logs Conclusion What is Project Leyden? Project Leyden is an OpenJDK project that has been in the making since 2022. The project aims to leverage CDS (Class Data Sharing) and Ahead-of-Time optimizations to selectively shift and constrain computations from runtime to some point in time — to an earlier phase, for instance. The ultimate goal is to create fully static images under the closed-world constraint. The closed-world constraint enables more powerful startup optimizations, as we can see with GraalVM Native Image. But it doesn’t go well with Java’s dynamism. So, currently, the Project Leyden team explores weaker constraints that can be applicable to a wider range of applications. What can Project Leyden do at this stage of its development? It observes application behavior during the trial run and performs certain computations based on the observations. After the trial run, it creates a CDS archive with class metadata, method profilers, and compiled code (such as methods frequently used during the trial run). As a result, when you perform a production run of your application, it uses the data from the CDS archive and starts faster. How much faster? Let’s find out! Use Project Leyden with Spring Boot For my experiment, I took Spring Petclinic for Spring Boot 3.3. It starts in about 3.5 seconds on my machine, an old M1 MacBook Air. The framework version is important because of first-class CDS support and CDS-friendly unpacked deployment (more on it below). First, create a jar with the following command: mvn -Dmaven.test.skip=true clean package Then, create an exploded jar, which is recommended to be used in production by Spring: java -Djarmode=tools -jar target/spring-petclinic-3.3.0-SNAPSHOT.jar extract When you use the -Djarmode=tools utility, Spring extracts the application into a directory using various layouts. In the case of a default layout, the directory will contain a lib subdirectory with libraries, and the application JAR with application classes and a manifest that references libraries in the lib folder. Using this layout with non-nested jars will enable you to benefit more from Leyden capabilities. Now, let’s make use of Leyden. We need to conduct a trial run: java -XX:CacheDataStore=SpringPetclinic.cds -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar Two files will be created: SpringPetclinic.cds with class metadata, heap objects, and profiling data; SpringPetclinic.cds.code with AOT-compiled methods (it is planned to merge this file with SpringPetclinic.cds) in the future. After that, conduct a production run of the application with the same command: java -XX:CacheDataStore=SpringPetclinic.cds -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar |\ _,,,--,,_ /,`.-'`' ._ \-;;,_ _______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______ | | '---''(_/._)-'(_\_) | | | | | | | | | | _ | ___|_ _| | | | | |_| | | | __ _ _ | |_| | |___ | | | | | | | | | | \ \ \ \ | ___| ___| | | | _| |___| | _ | | _| \ \ \ \ | | | |___ | | | |_| | | | | | | |_ ) ) ) ) |___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / / ==================================================================/_/_/_/ :: Built with Spring Boot :: 3.3.0 2024-06-29T11:02:16.569+03:00 INFO 11092 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting PetClinicApplication v3.3.0-SNAPSHOT using Java 24-leydenpremain with PID 11092 (/Users/ekaterina/Downloads/spring-petclinic-main/spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar started by ekaterina in /Users/ekaterina/Downloads/spring-petclinic-main) 2024-06-29T11:02:16.571+03:00 INFO 11092 --- [ main] o.s.s.petclinic.PetClinicApplication : No active profile set, falling back to 1 default profile: "default" 2024-06-29T11:02:16.770+03:00 INFO 11092 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2024-06-29T11:02:16.774+03:00 INFO 11092 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 4 ms. Found 2 JPA repository interfaces. 2024-06-29T11:02:16.934+03:00 INFO 11092 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2024-06-29T11:02:16.936+03:00 INFO 11092 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2024-06-29T11:02:16.936+03:00 INFO 11092 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.24] 2024-06-29T11:02:16.942+03:00 INFO 11092 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2024-06-29T11:02:16.942+03:00 INFO 11092 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 365 ms 2024-06-29T11:02:17.019+03:00 INFO 11092 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2024-06-29T11:02:17.029+03:00 INFO 11092 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:07c35cd0-80c3-40fc-9bd5-34a0c390afff user=SA 2024-06-29T11:02:17.030+03:00 INFO 11092 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2024-06-29T11:02:17.069+03:00 INFO 11092 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2024-06-29T11:02:17.071+03:00 INFO 11092 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.5.2.Final 2024-06-29T11:02:17.073+03:00 INFO 11092 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2024-06-29T11:02:17.090+03:00 INFO 11092 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2024-06-29T11:02:17.179+03:00 INFO 11092 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) 2024-06-29T11:02:17.179+03:00 INFO 11092 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2024-06-29T11:02:17.220+03:00 INFO 11092 --- [ main] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used. 2024-06-29T11:02:17.490+03:00 INFO 11092 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 14 endpoints beneath base path '/actuator' 2024-06-29T11:02:17.525+03:00 INFO 11092 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' 2024-06-29T11:02:17.536+03:00 INFO 11092 --- [ main] o.s.s.petclinic.PetClinicApplication : Started PetClinicApplication in 1.01 seconds (process running for 2.429) As you can see, the application started in 1 second, which is more than three times faster than without Leyden! Looking into the logs If you are curious to know what happens behind the scenes of Project Leyden, logging will help. So, let’s conduct two more trial and production runs with enabled logging. Note that before creating new .cds* files, the ones generated from previous trial runs should be deleted: rm -fv SpringPetclinic.cds* Alright, let’s conduct a trial run and log the process of the CDS archive creation with the -Xlog:cds=debug:file=log/cds.log option: java -XX:CacheDataStore=SpringPetclinic.cds -Xlog:cds=debug:file=log/cds.log -Dspring.context.exit=onRefresh -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar Now, modify the command for launching a product run to log the number of loaded classes with -Xlog:class+load=info:file=log/class-load.log: java -XX:CacheDataStore=SpringPetclinic.cds -Xlog:class+load=info:file=log/class-load.log -jar spring-petclinic-3.3.0-SNAPSHOT/spring-petclinic-3.3.0-SNAPSHOT.jar To see how many classes were loaded in total, run: cat log/class-load.log | wc -l 17375 This file also contains data on classes loaded from the shared archive. Run the following command: grep -o 'source: shared' -c log/class-load.log 16733 As you can see, 96% of classes got into the archive! But Leyden is not only about loading classes. As mentioned above, the archive also stores profiling data and compiled code. More detailed information on the archive generation can be found in the cds.log file. Conclusion Project Leyden is flourishing. The preliminary results in startup and warmup times are tangible, and more is yet to come! We at BellSoft are looking forward to the stable release. But what if you need to reduce startup now? Take a look at CDS. It is a production-ready feature that enables you to reduce startup by up to 54% depending on the setup. Most JDK distributions, including Liberica JDK, are provided with a pre-packaged CDS archive, and using it is pretty straightforward. Read more in the article How to use CDS with Spring Boot. GraalVM Native Image enables the generation of native images that start up almost instantly, but may require code refactoring. Find out more in Spring Boot with GraalVM Native Image. Project CRaC is an OpenJDK API that enables you to reduce Java application startup to milliseconds and preserve the power of JIT for dynamic performance optimization. Learn more about this solution in What is CRaC? article. Want to know more about improving the performance of your Java apps? Subscribe to our newsletter! - [An overview of Java 23 features](https://bell-sw.com/blog/an-overview-of-java-23-features/): JDK 23 entered a Rampdown Phase One on June 6, which means that the feature set is frozen. It’s high time we look at them more closely! This release contains 12 Java Enhancement Proposals (JEPs) with new, enhanced, or deprecated features. Table of Contents New features JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) JEP 467: Markdown Documentation Comments JEP 476: Module Import Declarations (Preview) Enhanced features JEP 466: Class-File API (Second Preview) JEP 469: Vector API (Eighth Incubator) JEP 473: Stream Gatherers (Second Preview) JEP 474: ZGC: Generational Mode by Default JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview) JEP 480: Structured Concurrency (Third Preview) JEP 481: Scoped Values (Third Preview) JEP 482: Flexible Constructor Bodies (Second Preview) Deprecated features JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal Get the performance of newer Java versions without migration New features JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) JEP 455 aims to enhance pattern matching by enabling the developers to use primitive types in all pattern contexts, as well as with instanceof and switch, which will make Java patterns more expressive. This feature also eliminates the risks of data loss due to unsafe casts. Up until now, instanceof type tests were limited to reference types, and there was no convenient way to check primitive values for safety of conversion. As a result, a primitive value could be silently converted without an exception. But now, the instanceof type test operator allows for primitive types. If a value can be safely converted, instanceof will report true. Otherwise, it will report false. JEP 467: Markdown Documentation Comments JEP 467 will enable the developers to write JavaDoc documentation comments in Markdown. This will facilitate reading and writing API documentation comments. But this feature doesn’t aim to substitute HTML and JavaDoc tags, rather, it will allow using the combination of the three. JEP 476: Module Import Declarations (Preview) JEP 476 simplifies module imports by allowing the developers to import one module instead of explicitly importing its packages. For instance, instead of import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; Or import java.util.*; import java.util.function.*; import java.util.stream.*; We will simply write import module java.base Enhanced features JEP 466: Class-File API (Second Preview) Class-File API was introduced in JDK 22 as a Preview feature. The goal is to equip the Java platform with a standard API for parsing, generating, and transforming class files. This API will evolve together with the class-file format and will enable the Java platform components and framework to rely on this API instead of third-party libraries. JEP 466 introduces a Second Preview of the feature with a few improvements based on the feedback. JEP 469: Vector API (Eighth Incubator) Vector API was first introduced in JDK 16. JEP 469 is the Eighth Incubator of the feature aiming to equip the developers with a way to write vector algorithms that reliably compile at runtime to optimal vector instructions on supported CPU architectures. This will provide better performance of scalar operations than with auto-vectorization. JEP 469 re-incubates Vector API without changes. JEP 473: Stream Gatherers (Second Preview) Stream Gatherers were introduced in JDK 22 to enhance Stream API with the support for custom intermediate operations. Before Stream Gatherers, developers had to create separate classes or methods to use required logic in already existing intermediate operations. But this feature is not about adding more intermediate operations to the Stream API. Instead, developers get one new intermediate operation called Stream::gather(Gatherer) that allows for processing streams in a user-defined fashion. JEP 473 re-previews the feature without changes. JEP 474: ZGC: Generational Mode by Default Generational mode for Z Garbage Collector was introduced in Java 21 with JEP 439. Before that, ZGC stored all objects together, and collected all objects at once. With generational mode, ZGC maintains separate collections for young and old objects, which means that it can collect young objects more frequently. So, the CPU overhead is lower, and the amount of released memory is bigger. JEP 474 makes generational mode default for ZGC. The non-generational mode, which is inferior to generational one in most cases, will be deprecated in this release and removed in future releases. JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview) This feature was introduced in JDK 21 as a Preview. It enables programmers who just started learning Java to write simple single-class programs without enterprise-grade features, and then extend these programs as their knowledge grows. But what are Implicitly Declared Classes and Instance Main Methods? Instance main methods are not static and need not be public and have a String[] parameter. Thus, the standard Hello, World! program can be simplified to: class HelloWorld { void main() { System.out.println("Hello, World!"); } } Implicitly declared classes have only a default zero-parameter constructor, reside in an unnamed package, and cannot be referenced by name. Every implicit class must contain a main method and represent a standalone program. As a result, the Hello, World! Code can be further simplified to: void main() { System.out.println("Hello, World!"); } JEP 477 introduces a Third Preview of the feature with two major improvements: Implicitly declared classes automatically import three static methods from a new top-level class java.io.IO: public static void println(Object obj) public static void print(Object obj) public static String readln(String prompt) This way, students don’t have to deal with System.in and System.out and all related concepts. So, a simple interactive program will look like that: void main() { String name = readln("Please enter your name: "); print("Pleased to meet you, "); println(name); } Another addition is the automatic import of the java.base module. So the APIs in the commonly used packages will be allowed for the use in the body of an implicitly declared class as if they were imported. JEP 480: Structured Concurrency (Third Preview) Structured concurrency is aimed at providing the developers with a more reliable and transparent way of writing concurrent code. The approach is based on a principle of a single unit of work, i.e., if the tasks are split into subtasks, then subtasks should return to the parent task’s code block. This way, The task-subtask relationship will be demonstrated clearly in the code struction, which enhances observability; If any of the subtasks fail or a thread running the tasks is interrupted before join(), other subtasks are canceled, which promotes code reliability. Structured concurrency was first introduced in JDK 19. JEP 480 re-previews the feature without changes. JEP 481: Scoped Values (Third Preview) Scoped values were first introduced in JDK 20. Scoped values are a more robust and memory-efficient alternative to thread-local variables, especially when used together with virtual threads. Scoped values enable a method to share immutable data with its callees within a thread and with child threads. A scoped value is available only until the method that called it finishes, which should prevent long-term memory leaks associated with thread-local variables, whose values are retained for the thread lifetime or until the remove method is called). Note that scoped values should be used for one-way transmission of immutable data. They are not suitable for two-way transmission. JEP 481 re-previews the feature with one change: the removal of the ScopedValue.getWhere method, which is substituted with a new functional interface that allows JVM to deduce whether a checked exception might be thrown. JEP 482: Flexible Constructor Bodies (Second Preview) This feature was introduced in JDK 22 as JEP 447: Statements before super(...). Its goal is to reduce code verbosity and complexity by enabling the developers to place statements before an explicit constructor invocation. Note that the feature won’t break the natural top-down order initialization. JEP 482 introduces a Second Preview of the feature with new name and one substantial change: a constructor body can now initialize fields in the same class before explicitly invoking a constructor. As a result, a constructor in a superclass won’t be able to execute code which sees the default field value in the subclass. Deprecated features JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal The aim of the sun.misc.Unsafe class has been to perform low-level operations in the JDK as it contains methods for accessing on-heap and off-heap memory. These methods can help to increase the performance in some specific scenarios, but only if exhaustive safe checks are performed along the way. Otherwise, the use of these methods may lead to unexpected application behavior, JVM crashes, or performance deterioration. Unfortunately, many libraries use sun.misc.Unsafe, but not all of them perform due safe checks. To address this issue, two replacement APIs were introduced: Variable Handles for accessing on-heap memory and Foreign Function & Memory API for the off-heap memory. These API’s are inherently more stable and reliable and should be used instead of sun.misc.Unsafe. Thanks to the introduction of these APIs, it is not possible to deprecate sun.misc.Unsafe with JEP 471 for removal in future releases. Get the performance of newer Java versions without migration Early-access builds of JDK 23 are available for you to experiment with. Although Java 23 is a non-LTS release, it is worth trying out new features, especially if you are planning to use the next LTS version due 2025. What if some of your services run on older JDK versions 8 or 11, and upgrading is off the table for now? You can get the performance of JDK 17 without migration with Liberica JDK Performance Edition! It couples JDK 8 or 11 and JVM 17 and enables you to boost the application performance without major code changes. Contact us if you would like to know more about the solution. Contact us - [Liberica JDK 8u422, 11.0.24, 17.0.12, 21.0.4, 22.0.2 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u422-11-0-24-17-0-12-21-0-4-22-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 8u421, 11.0.23.0.1, 17.0.11.0.1, and 21.0.3.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u422, 11.0.24, 17.0.12, 21.0.4, and 22.0.2 with non-critical fixes and general improvements. The release contains 1352 fixes and backports overall. BellSoft participated in eliminating 32 issues in all releases. Table of Contents How to keep your runtime secure The summary of fixes List of security issues fixed Summary of fixes in Liberica JDK Upstream changes: highlights Supported platforms Enjoy the most stable runtime! How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 6 security issues (CVEs) fixed. 54 total security fixes (+ 18 additional non-security fixes) in CPU release: in Liberica 6u431: 7 security fixes + 6 additional fixes; in Liberica 7u431: 8 security fixes + 9 additional fixes; in Liberica 8u421: 9 security fixes + 3 additional fixes; in Liberica 11.0.23.0.1: 11 security fixes; in Liberica 17.0.11.0.1: 10 security fixes; in Liberica 21.0.3.0.1: 9 security fixes. In addition, PSU releases include a total of 1280 bugs and backports fixed: in Liberica 8u422: 9 security fixes (+ 1 in FX) + 37 additional fixes (+ 6 in FX); in Liberica 11.0.24: 11 security fixes (+ 1 in FX) + 326 additional fixes (+ 115 in FX); in Liberica 17.0.12: 10 security fixes (+ 1 in FX) + 241 additional fixes (+ 5 in FX); in Liberica 21.0.4: 9 security fixes (+ 1 in FX) + 329 additional fixes (+ 5 in FX). in Liberica 22.0.2: 9 security fixes (+ 1 in FX) + 151 additional fixes (+ 12 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2024-21147 7.4 hotspot compiler network high none none unchanged high high none CVE-2024-21145 4.8 client-libs 2d network high none none unchanged low low none CVE-2024-21140 4.8 hotspot compiler network high none none unchanged low low none CVE-2024-21144 3.7 core-libs java.util network high none none unchanged none none low CVE-2024-21131 3.7 hotspot runtime network high none none unchanged none low none CVE-2024-21138 3.7 hotspot runtime network high none none unchanged none none low Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 22 CVE-2024-21147 • • • • • CVE-2024-21145 • • • • • CVE-2024-21140 • • • • • CVE-2024-21144 • • CVE-2024-21131 • • • • • CVE-2024-21138 • • • • • Upstream changes: highlights This CPU release contains a number of important fixes and updates, including: JDK-8316138: Add GlobalSign 2 TLS root certificates JDK-8331770: Fix inconsistent behavior in com.sun.jndi.ldap.Connection::createSocket JDK-8327808: Disable DTLS 1.0 JDK-8333011: Fixed problems resolution of symbolically linked native libraries by dpkg JDK-8264322: Generate CDS archive when creating custom JDK image JDK-8328638: Fallback option for POST-only OCSP request Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [How to Generate an SBOM for Java: A Comprehensive Guide](https://bell-sw.com/blog/how-to-generate-an-sbom-for-java-a-comprehensive-guide/): According to Gartner’s prediction, 45% of enterprises worldwide will have faced an attack on their software supply chain by 2025. In most cases, hackers target unpatched vulnerabilities, which can be plentiful, considering that an ordinary enterprise project usually uses hundreds of dependencies. A software bill of materials (SBOM) is an indispensable tool for building a DevSecOps strategy as it enables you to inspect the state of your IT infrastructure and form a risk mitigation plan. In addition, some legislations already demand that software vendors provide a software bill of materials. This article examines what an SBOM is, when you need it, and how to generate an SBOM for a Java application packed as a jar file using popular open source tools. BellSoft supplies its customers with software BOMs for Liberica JDK so enterprises have a transparent and secure Java development environment. Contact us to know more about the service Table of Contents What is an SBOM? When to Generate an SBOM Best Tools for Generating an SBOM Syft CycloneDX Generator Tern Kubernetes bom Comparative table of tools for SBOM generation How to Generate an SBOM Using Free and Open-Source Tools How to use Syft How to use CyclonDX Generator How to generate a Java SBOM with a Maven plugin How to generate a Java SBOM with a Gradle plugin How to generate an SBOM for Spring Boot 3.3 Frequently Asked Questions (FAQ) What is an SBOM? How do I create an SBOM? How to generate an SBOM for Java? What does an SBOM include? How can I get an SBOM for Java? Conclusion What is an SBOM? A software bill of materials is an inventory of all dependencies used to build a software product. It is similar to a traditional bill of materials listing all raw materials, assemblies, and components utilized for product manufacture. An SBOM includes open-source and proprietary software. But it doesn’t imply the disclosure of source code. The code may remain confidential at the vendor’s discretion. To continue the comparison with a BOM: giving a list of materials doesn’t mean revealing the manufacturing processes. Why is an SBOM necessary? Its purpose depends on your role. The Operations engineers can control the integrity of dependencies in the project, monitor vulnerabilities, and react to them promptly. On the other hand, the purchasers can make an informed decision about the product. All in all, an SBOM has the following benefits for a DevSecOps strategy: Prevent hidden vulnerabilities. According to Snyk, an average project contains about 49 vulnerabilities in direct and indirect dependencies. With an SBOM, you can see if there are components with vulnerabilities and take mitigating actions. Minimize licensing risks. Any third-party code we use in development is licensed. An SBOM contains information about component licensing, enabling a business to avoid legal risks and protect intellectual property. Evaluate the state of software components. An SBOM provides data about component versions, so you can determine whether you use a fresh or outdated dependency. Promote vendor accountability. An SBOM supplied with a software product proves that the supplier ensures the security and licensing integrity of their product. Ensure legislative compliance. The U.S. President’s Executive Order No. 14028 on Improving the Nation’s Cybersecurity dictates that U.S. state agencies must use only software supplied with a software bill of materials. When to Generate an SBOM Software suppliers or Operations engineers generate an SBOM for their software product and update it with every release. So, if there’s a change or an update to the product, an SBOM is generated anew. You can create an SBOM at any time of the software development cycle: at the source code stage, at build time, at runtime, or during the software inspection. If the application is deployed as a container image or a set of container images, an SBOM should describe all container images, including the relationship between them. The NTIA recommends generating an SBOM at build time, but if some software components are not part of the built-time SBOM generation, it is possible to create a post-build SBOM for them. SBOMs must be in a commonly accepted, universal form for smooth integration into corporate security processes. As such, the SBOM must be generated in one of these standard industry formats: Software Package Data eXchange (SPDX) by the Linux Foundation, CycloneDX by the Open Worldwide Application Security Project (OWASP) Foundation, Software Identification (SWID) tags defined by the ISO/IEC 19770-2:2015 standard. Best Tools for Generating an SBOM Numerous tools for the SBOM generation are available, both free and commercial. Some tools generate an SBOM during build time, some at the post-build stage. Some solutions scan the open-source dependencies, and others can also analyze proprietary libraries. In addition, there are software composition analysis (SCA) tools identifying and gathering information about open-source components. This data can be further used for building an SBOM. Below is the list of the most popular free and open-source SBOM-generation tools. Syft Syft is a CLI tool and a Go library for SBOM generation developed by Anchore. Syft can generate an SBOM for container images, filesystems, and archives. Supported formats are CycloneDX, SPDX, and Syft’s own format. It is also possible to sign an SBOM using in-toto attestation specification to prove its authenticity. Syft is a free and easy-to-use tool available for Linux, macOS, and Windows. In case you need enterprise support, it is provided by Anchore. CycloneDX Generator CycloneDX Generator (cdxgen) is a CLI tool and Node.js library for generating SBOMs in CycloneDX format (converting between formats is also possible). cdxgen supports multiple programming languages, project types, package formats, and BOM formats, including Software BOM, Cryptography BOM, Software-as-a-Service BOM, etc. CycloneDX Generator was developed with enterprise projects in mind, so it integrates easily with CI/CD pipelines and is suitable for applications, container images and even Linux and Windows hosts. It has extensive documentation and optional commercial support by AppThreat. Tern Tern is a tool written in Python that can produce a Software Bill of Materials for Docker container images and Dockerfiles. You need to have Python or Docker installed to be able to use it. A GitHub Action is available. In addition, Tern can be deployed as a Kubernetes Job. Tern supports multiple SBOM formats, including CycloneDX and SPDX. The project is free and community-driven, but commercial support is not available. Kubernetes bom Kubernetes bom is a tool written in Go aimed at creating SBOMs for Kubernetes projects. It can scan container images, single files, directories, and other sources, and generates a bill of materials in SPDX format. In addition, Kubernetes bom includes a license classifier and can recognize more than 400 licenses. Currently, there’s no commercial support for Kubernetes bom, but as this project is part of the Kubernetes ecosystem and backed by the LinuxFoundation, it may become more widespread in the future. Comparative table of tools for SBOM generation Feature Syft CycloneDX Generator Tern Kubernetes bom Supported components Project dependencies, OS packages Project dependencies, OS packages Project dependencies, OS packages Project dependencies, OS packages Container image scanning Yes Yes Yes (Dockerfiles can be scanned without building an image) Yes Output formats CycloneDX, SPDX, Syft, GitHub JSON CycloneDX, SPDX CycloneDX, SPDX, human-readable format, JSON, HTML, YAML SPDX in-toto attestation Yes Yes No Yes Enterprise support Yes Yes No No How to Generate an SBOM Using Free and Open-Source Tools This section includes a guide on using two popular open-source tools, Syft and CyclonDX Generator, for creating an SBOM for your project. We will use Java projects and container images as an example. You can also take your own project and follow along. Note that to generate jar files and docker images for examples below you need to have JDK 21 installed. You can download Liberica JDK 21 available for multiple platforms or use Linux repositories or a package manager (SDKMAN, Homebrew, Scoop, or winget) to get the binary. How to use Syft First of all, we need to install Syft. You can use curl to download a binary: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin Alternatively, you can install Syft via package managers such as Homebrew, Chocolatey, Scoop, or Nix. For instance, a command for installing Syft with Homebrew: brew install syft To verify the installation, run the following command: syft version Application: syft Version: 1.8.0 BuildDate: 2024-06-24T15:27:29Z GitCommit: Homebrew GitDescription: [not provided] Platform: darwin/arm64 GoVersion: go1.22.4 Compiler: gc Now, let’s generate an SBOM for a jar file. To create an SBOM for a jar file using Syft, we need to specify the path to the jar, output format (the -o flag), and, optionally, a path to the output file (the --file flag). We will generate an SBOM in CycloneDX format. So, the resulting command is as follows: syft target/application.jar --output cyclonedx-json=sbom-syft.json You can navigate to the generated app-sbom-syft.cdx.json file and inspect its contents. Let’s generate an SBOM for a container image: syft your-docker-image --output cyclonedx-json=oci-sbom-syft.json The resulting file will contain all the data we saw before when generating an SBOM for a jar file plus information about the OS packages of the underlying distro. How to use CyclonDX Generator You can install cdxgen via the npm repository: npm install -g @cyclonedx/cdxgen Alternatively, use Homebrew: brew install cdxgen Verify installation by running the following command: cdxgen --version 10.8.0 To generate an SBOM for a Java project, go to the project root directory and run: cdxgen -t java -o app-bom-cdxgen.json The file will be generated in the root directory. We specified the project type (-t java) so that cdxgen can detect the build system and build an SBOM accordingly. By default, cdxgen doesn't resolve package licenses as it is a time-consuming operation. But you can enable this feature by setting the environment variable FETCH_LICENSE to true: FETCH_LICENSE=true cdxgen -t java -o app-bom-cdxgen.json You can also generate an SBOM for a JAR file: cdxgen -t jar -o app-bom-cdxgen.json How to generate a Java SBOM with a Maven plugin You can use a CylconeDX plugin for Maven to generate an SBOM for a Java application. Let’s look at the plugin configuration in pom.xml with the default settings: org.cyclonedx cyclonedx-maven-plugin 2.9.1 package makeAggregateBom library 1.6 true true true true true false false true all bom ${project.build.directory} false Here, we set the makeAggregateSbom goal, which creates an aggregate BOM at build root with dependencies from the whole multi-modules build, and a BOM for each module. You can change the settings as you see fit. After you create a jar file for your application, two files, bom.json and bom.xml will appear in the target directory. The file includes both direct and transitive dependencies. There’s also an SPDX plugin for Maven. The default configuration is as follows: org.spdx spdx-maven-plugin 0.7.3 build-spdx createSPDX After that, run mvn spdx:createSPDX The SPDX file will be generated in the ./target/site directory with a name pattern {groupId}_{artifactId}-{version}.spdx. Note that the file doesn’t contain transitive dependencies. Perhaps, this functionality will be added in future plugin releases. How to generate a Java SBOM with a Gradle plugin There’s also a CycloneDX plugin for Gradle. You can add it to your build.gradle file and configure it as needed: plugins { id 'org.cyclonedx.bom' version '1.8.2' } Possible configuration: cyclonedxBom { includeConfigs = ["runtimeClasspath"] skipConfigs = ["compileClasspath", "testCompileClasspath"] skipProjects = [rootProject.name, "yourTestSubProject"] projectType = "application" schemaVersion = "1.5" destination = file("build/reports") outputName = "bom" outputFormat = "json" includeBomSerialNumber = true includeLicenseText = false componentVersion = "2.0.0" } To create an SBOM, run gradle cyclonedxBom The resulting file will contain information on direct and transitive dependencies. There’s also an SPDX plugin for Gradle, which is currently under active development. How to generate an SBOM for Spring Boot 3.3 Spring Boot 3.3 comes with support for SBOMs using the CyclonDX Generator Maven plugin under the hood. Creating an SBOM for your Spring Boot 3.3 project is as easy as adding a plugin to pom.xml if you use Maven: org.cyclonedx cyclonedx-maven-plugin Or to build.gradle if you use Gradle: plugins { id 'org.cyclonedx.bom' version '1.8.2' } Now, everytime you rebuild your project, aт SBOM will be generated in the META-INF/sbom directory. This document contains both direct and transitive dependencies, as you can see from a snippet below: "dependencies" : [ { "ref" : "pkg:maven/org.springframework.boot/spring-boot-starter-actuator@3.3.0?type=jar", "dependsOn" : [ "pkg:maven/org.springframework.boot/spring-boot-starter@3.3.0?type=jar", "pkg:maven/org.springframework.boot/spring-boot-actuator-autoconfigure@3.3.0?type=jar", "pkg:maven/io.micrometer/micrometer-observation@1.13.0?type=jar", "pkg:maven/io.micrometer/micrometer-jakarta9@1.13.0?type=jar" ] } You can also expose an SBOM via Spring Boot Actuator. For that purpose, add the actuator dependency to pom.xml: org.springframework.boot spring-boot-starter-actuator Or build.gradle: dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' } Then, enable the sbom endpoint in application.properties: management.endpoints.web.exposure.include=health,sbom After that, when you build your application and run it from the jar file, you can visit localhost:8080/actuator/sbom and see a list of ids for SBOMs available for your project. You can use the ids to explore the given SBOM. For instance, by visiting localhost:8080/actuator/sbom/application, you will see a detailed bill of materials for your app. It is also possible to include additional SBOMs. For instance, as BellSoft provides an SBOM for Liberica JDK, you can specify the path to Liberica JDK SBOM in application.properties: management.endpoint.sbom.additional.jvm.location=file:/path/to/sbom.json Liberica JDK SBOM is provided in CycloneDX format, so Spring Boot will automatically detect its type. SBOMs and Cloud Native Buildpacks When you generate a container image with Spring Boot that uses Paketo buildpacks under the hood, the SBOMs for each layers will be created automatically using Syft. You can extract them from the container image with the help of the pack utility and the pack sbom download command: pack sbom download your-container-image -o sbom-layers Frequently Asked Questions (FAQ) What is an SBOM? A Software Bill of Materials or SBOM is a list of all dependencies used to build a software product. How do I create an SBOM? You can create an SBOM manually or using open-source tools that will generate an SBOM automatically. How to generate an SBOM for Java? Many SBOM generators support Java. CycloneDX Generator also provides a plugin for Maven projects to create an SBOM automatically when you rebuild the project. What does an SBOM include? An SBOM includes data on software components: supplier name, component name and version, dependency relationship, as well as author of SBOM data and timestamp (date and time of SBOM generation). How can I get an SBOM for Java? You can generate an SBOM for Java manually or ask your vendor whether they provide BOMs for their JDK distribution. BellSoft provides SBOMs for Liberica JDK. Conclusion A software bill of materials is indispensable for securing a software supply chain. As a DevSecOps engineer, you must be sure of your project’s integrity. As a customer, you have the right to demand an SBOM from your software vendor. If you develop a Java project, you will benefit from having a vendor-provided SBOM for your JDK. Contact us to know how to receive an SBOM for Liberica JDK. Contact us - [BellSoft adds AArch64 support to Liberica JDK Performance Edition ](https://bell-sw.com/news/bellsoft-adds-aarch64-support-to-liberica-jdk-performance-edition/): BellSoft, an OpenJDK provider that delivers the most complete Java experience for the cloud, released Liberica JDK Performance Edition for AArch64. Aarch64 support is essential for enterprise clients and developers using ARM instances in the cloud or Macs based on ARM architecture and still working with mature LTS releases. Liberica JDK Performance Edition for AArch64 instantly improves Java 8 & 11 performance and cuts cloud spending. The BellSoft team advises upgrading to the latest Liberica JDK LTS to enable contemporary Java features for development. However, migration often needs to catch up, and staying on older JDK versions is a choice for some enterprises. Getting the functionalities of modern Java is a valuable option delivered by BellSoft in the Liberica JDK Performance Edition solution via simple installation when skipping an immense migration. Liberica JDK Performance Edition bridges JDK 8 or 11 with JDK 17, leveling up performance in lower latency; less RAM consumption; faster startup; better generated JIT code; compression and decompression. The Spring PetClinic sample application results for workloads running on Liberica JDK Performance Edition with AArch64 architecture demonstrated the advancements of: Faster application response: by 16.9% on JDK 8 and 10% on JDK 11; Shorter G1 GC pauses: by 70% on JDK 8 and 46% on JDK 11; Less RAM need with G1 GC: by 12% on JDK 8 and 25% on JDK 11; Improved startup time: 7.7% on JDK 8 and 7.6% on JDK 11. "Liberica JDK Performance Edition is a key element for effective Java development when staying on mature JDK 8 or 11 frameworks. Our near plans include upgrading to JVM 21 (without Virtual Threads support). Liberica JDK Performance Edition for AArch64 delivers modern JVM capabilities and is suitable for both x86_64 and AArch64 architectures. It allows double cost cuts in the cloud based on Liberica JDK Performance Edition and ongoing cost-saving offers for the AArch64 architecture by cloud providers."- said Aleksei Voitylov, BellSoft's CTO. Install Liberica JDK Performance Edition in four easy steps without code rewrite. About BellSoft BellSoft delivers the most complete Java experience with a more secure, reliable, and cost-effective approach to application development on any platform and in any environment. BellSoft is one of the leading contributors to the OpenJDK, and the only vendor that supports current LTS Java versions, legacy JDK 6 & 7 and Liberica NIK. Liberica JDK is the runtime of choice for VMware, Spring Framework, JetBrains, and millions of users worldwide. For more information, visit www.bell-sw.com. - [Creating a Bot for Documentation Assistance with Spring AI and Unstructured.io](https://bell-sw.com/blog/creating-a-bot-for-documentation-assistance-with-spring-ai-and-unstructured-io/): Spring AI enables the developers to integrate the power of AI into their projects with minimal effort as Spring does most of the magic behind the scenes. In this article, I will show you how to build an AI bot that answers a user's questions based on the provided set of documents. The application leverages the power of Spring AI and Unstructured.io, a solution for preparing texts for LLMs. The source code of the application is available on GitHub. Table of Contents What is a vector in AI What does a documentation assistance bot do What is Unstructured.io? Bot application structure The application.properties file BellSwBotApplication class UnstructuredConfig class UnstructuredResponse class UnstructuredClient class BotController class Conclusion What is a vector in AI A vector in AI is a set of numeric values, often represented as an array, where each value corresponds to a particular attribute or a feature of a data point in a multidimensional space. These values are typically double-precision floating-point numbers. Machine learning models use these vectors to classify the data, make predictions, and identify patterns by leveraging mathematical operations. A very simplified but straightforward example: if you take a vector for a word ‘mother’, take away a ‘woman’ attribute, and then add a ‘man’ attribute, you will get a vector for a word ‘father.’ But vectors can represent not only words, but phrases, sentences, or even larger text blocks. What does a documentation assistance bot do The purpose of a chatbot I created is to answer user questions about company products. But the bot shouldn’t make up answers. It should be based on the Retrieval-Augmented Generation (RAG) concept. RAG is a technique for increasing the reliability of LLM responses with facts fetched from some specific sources. In our case, the bot will use the existing company documentation. To do that, we need to break the whole documentation corpus into smaller units, each of which can be turned into an embedding (another name for a vector) by the AI model. As I used the ADA model provided by OpenAI, this unit can be max. 8,191 symbols long. But we don’t want to create embeddings manually as it poses certain challenges, such as the necessity to extract plain text and get rid of noise such as formatting. This is where Unstructured.io comes to the rescue. What is Unstructured.io? Unstructured.io is a solution that takes a document of any type (HTML, PDF, CSV, PNG, PPTX, etc.) and transforms it into an LLM-ready file free of artifacts. Unstructured can divide the text into smaller elements (or chunks) according to one of four smart chunking strategies: “Basic” combines sequential elements, “By title” preserves section boundaries, “By page” creates separate chunks for each page, “By similarity” combines topically similar sequential elements into chunks. Bot application structure My chatbot is a Gradle application written in Kotlin. The project is based on Java 21 and Spring AI 1.0.0-M1. I used Liberica JDK recommended by Spring as a Java runtime. You can download Liberica JDK 21 for your platform from the site or get it directly through IntelliJ IDEA. There are a few dependencies: dependencies { implementation(libs.jackson.module.kotlin) implementation(libs.kotlin.reflect) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.reactor) implementation(libs.kotlinx.serialization.json) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.core) implementation(libs.ktor.client.java) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.spring.ai.openai.spring.boot.starter) implementation(libs.spring.ai.pgvector.store.spring.boot.starter) implementation(libs.spring.boot.starter.web) testImplementation(libs.kotlin.test.junit5) testImplementation(libs.spring.boot.starter.test) testRuntimeOnly(libs.junit.platform.launcher) } The application.properties file First, let’s look at application configuration. The application.properties file includes the following configs: spring.application.name=bell-sw-bot spring.main.banner-mode=off spring.ai.vectorstore.pgvector.index-type=HNSW spring.ai.vectorstore.pgvector.distance-type=COSINE_DISTANCE spring.ai.vectorstore.pgvector.dimensions=1536 spring.ai.openai.api-key={openai_key} spring.ai.openai.chat.enabled=true spring.ai.openai.embedding.enabled=true spring.datasource.url=jdbc:postgresql://localhost:5432/postgres spring.datasource.username=postgres spring.datasource.password=password unstructured.key={unstructured_key} unstructured.endpoint=https://api.unstructuredapp.io/general/v0/general Let’s take not of the most important ones: spring.ai.vectorstore.pgvector.index-type defines the nearest neighbor search index type. HNSW creates a multilayer graph; other possible index types are described in the documentation. spring.ai.vectorstore.pgvector.distance-type defines a search distance type. COSINE_DISTANCE is the default setting, but if the vectors are normalized, you can use other types defined in the docs. spring.ai.vectorstore.pgvector.dimensions defines the size of the List of Doubles. spring.ai.openai.api-key contains the key for accessing OpenAI. spring.ai.openai.chat.enabled and spring.ai.openai.embedding.enabled enable OpenAI chat model and embedding model. unstructured.key and unstructured.endpoint are used to configure access to the Unstructured.io API. The application itself contains just a few classes. BellSwBotApplication class The BellSwBotApplication class is the entry point for our application. It contains the usual @SpringBootApplication annotation as well as @EnableConfigurationProperties that enables support for the classes annotated with @ConfigurationProperties (in my application, it is the UnstructuredConfig class). @SpringBootApplication @EnableConfigurationProperties(UnstructuredConfig::class) class BellSwBotApplication fun main(args: Array) { runApplication(*args) } UnstructuredConfig class The UnstructuredConfig class helps to configure access to the Unstructured.io API. @ConfigurationProperties("unstructured") class UnstructuredConfig(val key: String, val endpoint: String) The class is annotated with @ConfigurationProperties and transparent. Its namespace is “unstructured”, and it includes only two properties: key and endpoint. Both are defined in the application.properties file, as we saw above. The class can be injected as a usual Spring Bean, which we will later do in the UnstructuredClient class. UnstructuredResponse class The typealias UnstructuredResponse represents a List of UnstructuredDocument objects (It basically means I’m too lazy to type List all around the application). typealias UnstructuredResponse = List UnstructuredDocument is a simple data class that contains basic information about a text chunk: its type, ID, text block, and Metadata, which is a separate class containing document metadata such as file name, file type, and so on. The Metadata class also contains the asMap() method that I will later use to persist the metadata in the database. @Serializable data class UnstructuredDocument( val type: String, @SerialName("element_id") val elementID: String, val text: String, val metadata: Metadata ) @Serializable data class Metadata( @SerialName("category_depth") val categoryDepth: Long? = null, val languages: List, @SerialName("link_texts") val linkTexts: List? = null, @SerialName("link_urls") val linkUrls: List? = null, val filename: String, val filetype: String, @SerialName("parent_id") val parentID: String? = null, @SerialName("emphasized_text_contents") val emphasizedTextContents: List? = null, @SerialName("emphasized_text_tags") val emphasizedTextTags: List? = null, @SerialName("orig_elements") val origElements: String? = null, @SerialName("is_continuation") val isContinuation: Boolean? = null, ) { @Suppress("UNCHECKED_CAST") fun asMap(): Map = mapOf( "categoryDepth" to categoryDepth, "languages" to languages, "filename" to filename, "filetype" to filetype, "parentID" to parentID, "isContinuation" to isContinuation, ).filterValues { it != null } as Map } Right, we have text chunks with metadata. Now, we need to turn them into embeddings. So, each text chunk that we got from Unstructured.io needs to be sent to OpenAI, which will transform them into embeddings. But Spring has its own Document class used in Spring AI. Here is a snippet from it: public class Document implements Content { // omitted private final String id; private Map metadata; private String content; private List media; @JsonProperty( index = 100 ) private List embedding; // omitted So, we need to turn UnstructuredResponse into Spring AI Document. We will do that with the UnstructuredResponseDocumentAdapter class: class UnstructuredResponseDocumentAdapter(response: UnstructuredDocument) : Document(UUID.nameUUIDFromBytes(response.elementID.toByteArray()).toString(), response.text, response.metadata.asMap()) { override fun toString(): String = "UnstructuredResponseDocumentAdapter() ${super.toString()}" } This class receives three arguments: a UUID generated from the text ID returned by Unstructured.io, our text, and text metadata as Map. The next step is to save the resulting embedding. For that purpose, I use pgvector, which is a PostgreSQL extension for storing embeddings and performing vector similarity search.The embeddings are stored in a table with the following fields: A Spring Document object we got after converting our UnstructuredResponse documentation block to Spring-supported format, A corresponding embedding, A UUID, Metadata in JSON. UnstructuredClient class The UnstructuredClient class is responsible for sending texts to Unstructured.io and receiving LLM-ready text chunks. First, we need to create an HttpClient. I used Ktor HttpClient that enables the application to withstand huge loads. @Component class UnstructuredClient(val unstructuredConfig: UnstructuredConfig) { private val client = HttpClient(Java) { install(ContentNegotiation) { json(Json { allowStructuredMapKeys = true ignoreUnknownKeys = true }) } } The parse() method works with the Unstructured.io API. It takes 25 arguments, but for this application, there’s no need to configure most of them. They simply default to null. The only required argument is the MultipartFile sent to Unstructured.io. Below you will find a snippet of the method, the full signature can be found on GitHub. suspend fun parse( file: MultipartFile, chunkingStrategy: String? = null, combineUnderNChars: Int? = null, maxCharacters: Int? = null ) = client .submitFormWithBinaryData( url = unstructuredConfig.endpoint, formData = formData { file.apply { append("files", file.bytes, Headers.build { append(HttpHeaders.ContentDisposition, "filename=\"${file.originalFilename}\"") }) } chunkingStrategy?.apply { append("chunking_strategy", chunkingStrategy) } combineUnderNChars?.apply { append("combine_under_n_chars", combineUnderNChars.toString()) } maxCharacters?.apply { append("max_characters", maxCharacters.toString()) } } ) { accept(Json) contentType(FormData) header("unstructured-api-key", unstructuredConfig.key) } Let’s look more closely at the method body. The HttpClient submits a request (FormData type) to the URL endpoint provided by the UnstructuredConfig class). Then, it appends all arguments to the request: if the argument is present, it is added to the request. If it is null, then the Client does nothing and moves on to the next argument. Finally, when the request is formed, we add information about the content type (application/json), request type (FormData), and API key provided by the UnstructuredConfig class. BotController class All the building blocks of the application are ready, it’s time to describe user communication! The BotController class is a RestController configured in the following way: @RestController class BotController(val vectorStore: VectorStore, chatModel: ChatModel, val unstructuredClient: UnstructuredClient) { private val chatClient = ChatClient.create(chatModel) Note that we can’t directly inject ChatClient, which is used to communicate with OpenAI, but we can create it from ChatModel defined in the .properties file under spring.ai.openai.chat.enabled=true VectorStore is also defined in the .properties file under spring.ai.vectorstore.pgvector.index-type=HNSW spring.ai.vectorstore.pgvector.distance-type=COSINE_DISTANCE spring.ai.vectorstore.pgvector.dimensions=1536 Our BotController contains only two methods, but that’s all we need to answer user’s questions and save embeddings derived from our documentation in the database. Storing the document embeddings First, let’s create the storeDocument() method for sending documents to Unstructured.io (in a real-world scenario, it should be hidden behind the Spring Security wall to prevent users from sending random documents and storing them in our database). @PostMapping("/") suspend fun storeDocument(@RequestParam file: MultipartFile): ResponseEntity { val responses: UnstructuredResponse = unstructuredClient.parse( file = file, combineUnderNChars = 500, maxCharacters = 8191, includeOrigElements = false, chunkingStrategy = "basic", ) .body() val docs = responses.map(::UnstructuredResponseDocumentAdapter) vectorStore.add(docs) return ResponseEntity.of(Optional.of(Unit)) } This method receives a MultipartFile and hands it over to the UnstructuredClient class, which performs the communication with Unstructured.io, together with some other essential information: combineUnderNChars = 500 tells the Unstructured API that if for some reason, we get tiny pieces of text under 500 characters, they should be combined into one, maxCharacters = 8191 is the max size of a resulting text chunk, includeOrigElements = false instructs the API not to return the original document together with the chunks, chunkingStrategy = "basic" sets one of the chunking strategies we discussed earlier. After receiving a List of responses from Unstructured.io (UnstructuredResponse), we convert all responses to Spring Documents and save them to the database with vectorStore.add(docs) This small line of code actually contains tons of Spring magic under the hood. The method determines a model we use for creating embeddings, obtains embeddings for these Documents from the model, and saves them to VectorStore along with the source texts. Querying The Bot After we stored our documents, we can create the most exciting part of the bot: the user interface. In our case, it will just return a text, but it might be as complex as we need to integrate it anywhere. To achieve this goal, let’s write the query() method for processing user queries. Let’s look at it more closely: @GetMapping("/search") fun query(@RequestParam q: String): String { val query = SearchRequest.query(q).withTopK(3) val docs = vectorStore.similaritySearch(query) val information = docs.joinToString("\n---||---\n") { """TITLE: ${it.metadata["filename"]} |BODY: |${it.content} """.trimMargin() } val systemPromptTemplate = SystemPromptTemplate( """You are a helpful assistant. You find information in data provided below. The format of data is the following: --- TITLE: title of the source post BODY: text of the document or its part --- The delimiter between documents is "---||---" If you have to reference a document, reference it by name. Always reference a document where you found the information. If there is no information, you answer with a message "Sorry, I do not have such an information". Use the following information to answer the question: --- {information}""" ) val systemMessage = systemPromptTemplate.createMessage( mapOf("information" to information) ) val userMessagePromptTemplate = PromptTemplate(""""{question}"""") val model = mapOf("question" to q) val userMessage = UserMessage(userMessagePromptTemplate.create(model).getContents()) val prompt = Prompt(listOf(systemMessage, userMessage)) val response = chatClient.prompt(prompt).call().content() return response } The whole heavy-lifting process of searching for similar documents in our vector storage is hidden in these two lines of code: val query = SearchRequest.query(q).withTopK(3) val docs = vectorStore.similaritySearch(query) Here, we feed the user's question to Spring’s static method query() provided by SearchRequest and select three most relevant documents associated with the topic of the question. The similaritySearch() method hides the logic of searching for similar embeddings: an SQL query, query for OpenAI to create an embedding out of the user’s question, etc. The method returns a list of documents found in the database. After that, we take these documents and format them by adding a TITLE (which will contain the file name) and BODY (with the text block). And then we add them to the prompt. The systemPromptTemplate variable defines how we communicate with OpenAI. It includes the format of our data, specifies a delimiter, sets other essential requirements, and provides the document chunks that the bot uses to form an answer. These document chunks are the ones that Spring found for us and we formatted into the information block. Finally, we create a prompt we will return to the user. Conclusion As you can see, building an AI-backed application with Spring is not difficult, and if you have a working knowledge of basic Spring concepts, you will get a hang of Spring AI pretty quickly. If you want to read more tutorials on modern Spring development, subscribe to your newsletter! - [Liberica Native Image Kit 23.0.5, 23.1.4, and 24.0.2 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-5-23-1-4-and-24-0-2-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.5 for JDK 17, 23.1.4 for JDK 21, and 24.0.2 for JDK 22 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2024-21147 7.4 hotspot compiler network high none none unchanged high high none CVE-2024-21145 4.8 client-libs 2d network high none none unchanged low low none CVE-2024-21140 4.8 hotspot compiler network high none none unchanged low low none CVE-2024-21131 3.7 hotspot runtime network high none none unchanged none low none CVE-2024-21138 3.7 hotspot runtime network high none none unchanged none none low CVE-2024-27983 7.4 node.js nghttp2 network low none none unchanged none low high Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [How to build and release GraalVM native images using GitHub Actions](https://bell-sw.com/blog/how-to-build-and-release-graalvm-native-images-using-github-actions/): In this article, I’m going to demonstrate how to create a GitHub Action for building Java native images for Linux, macOS, and Windows. The source code is available on GitHub. Table of Contents What is PlantUML and why I chose this project for demonstration Collecting metadata for Native Image: what could go wrong? Add support for Native Image to the build script Create a GitHub Action workflow file for GraalVM Native Image Conclusion What is PlantUML and why I chose this project for demonstration As an example application, I will use PlantUML. It is a GUI Java Swing application that enables you to create diagrams from plain text. You can create diagrams on the website or use PlantUML as a GUI desktop application. If you want to code along, you can download the JAR file with PlantUML GPLv2 here. Note that if you want to run PlantUML on macOS, you will need to install Graphviz. If you use Homebrew, you can do that by running brew install graphviz. Let’s see PlantUML in action. Create a *.puml file in the same directory where PlantUML JAR resides with the following content: @startuml skin rose actor User actor Administrator User -> [Customer] [Customer] -> [Shipment] [Customer] -> [Calculator] [Admin] -> Customer [Admin] -> Shipment @enduml Run the JAR to verify that the app works: java -jar plantuml-gplv2-1.2024.6.jar The following window will pop up. PlantUML User Interface By clicking on the file, you should see a generated diagram. Example Diagram with PlantUML So why did I choose this program to demonstrate GraalVM Native Image? Firstly, using GraalVM Native Image with PlantUML resulted in two times faster startup and operation of the application. Another reason for choosing this program was to show how to efficiently tackle issues related to collecting full resource metadata for the Native Image compiler. Collecting metadata for Native Image: what could go wrong? As PlantUML is a Swing application, you can most conveniently turn it into a native image with Liberica Native Image Kit that comes with Swing unlike other GraalVM distributions. Download Liberica Native Image Kit Full for your platform. You can add Contents/Home/bin subdirectory to $PATH, or remember /Library/Java/LibericaNativeImageKit/bellsoft-liberica-vm-openjdk21-23.1.4/Contents/Home/ as the $NIK_HOME environment variable. In this tutorial, I will use $NIK_HOME. Before we go any further, a quick reminder: GraalVM Native Image is not compatible with the dynamic features of Java. All classes, resources, etc. should be reachable at build-time, or else they won’t get into the native executable. So if you will try to convert PlantUML into a native image with a standard command native-image -jar ./your.jar $NIK_HOME/bin/native-image --no-fallback --report-unsupported-elements-at-runtime -jar ./plantuml-gplv2-1.2024.6.jar The image will be built successfully. However, if you try to run it ./plantuml-gplv2-1.2024.6 my.puml Note: my.puml here is just an example of a PlantUML diagram. The application will fail to start. Apparently, some essential resources didn’t get into the native executable. One solution is to use the Tracing Agent, which tracks the usage of dynamic features and resources during application execution and then creates config files with the collected metadata in a specified output directory: $NIK_HOME/bin/java -agentlib:native-image-agent=config-output-dir=./puml-agent-data -jar plantuml-gplv2-1.2024.6.jar After that, you should place the config files to the META-INF/native-image/ directory on the class path, where they will be reachable to the native-image tool: $NIK_HOME/bin/native-image -H:ConfigurationFileDirectories=./puml-agent-data -jar plantuml-gplv2-1.2024.6.jar If you run the app now, it will start. Success? Yes and no. The problem is that the Tracing Agent does a good job at detecting resources, but to get a complete set of metadata, you need to run your application with different execution paths and covering as much possible input as possible. In my case, I faced another problem. When I tried to build native images with GitHub Actions, they all had only headless mode because I couldn’t specify the -Djava.awt.headless=true option. It is understandable because it’s not possible to run the app with a GUI during the build process on GitHub. But how do we tackle the issue? We can try to collect the resource metadata manually. But we want to minimize manual labor: after all, there can be thousands of resources! Luckily, there’s another way to provide metadata to native-image. Add support for Native Image to the build script Let’s update the build.gradle file with some essential configs. First, we need to add the official GraalVM plugin and the application plugin: plugins { java `maven-publish` signing id("org.graalvm.buildtools.native") version "0.10.2" application } One more block specifies the main class of the application (that’s why we needed the application plugin): application { mainClass = "net.sourceforge.plantuml.Run" } And finally, we add another block to configure Native Image: graalvmNative { binaries.all { resources.autodetect() buildArgs(listOf("-Djava.awt.headless=false")) } toolchainDetection = false } Here, we specify that for all binaries we build: We want resources to be autodetected and We pass an argument -Djava.awt.headless=false. How does the resource autodetection plugin work? By default, it detects resources available in the src/main/resources, but it can also detect the resources in other location specified in the script: sourceSets { main { java { srcDirs("build/generated/sjpp") } resources { srcDirs("build/sources/sjpp/java") include("**/graphviz.dat") include("**/*.png") include("**/*.svg") include("**/*.txt") } } } As a result, it finds all resources required for the application to run with a GUI, which turned out to be about 1,500 files for PlantUML. And in the case of this application, it detects way more resources than Tracing Agent. Theoretically, it might be possible to detect all these resources with Tracing Agent, but to do that, we must run the app through all possible scenarios and execution paths, which doesn’t seem practical, at least, not always. Therefore, using a plugin for resource autodetection enables us to collect all necessary metadata without much effort. Create a GitHub Action workflow file for GraalVM Native Image The next step is to write a GitHub Action file. Let’s look at the GitHub Actions workflow file (you can find it under the name native-image.yml in the repository). At the Build Native Image stage, it includes instructions on building binaries for four platforms using a GitHub Action for GraalVM. We specify explicitly that we want to use CE-based Liberica Native Image Kit for Java 21. In the case of this application, it is essential to use Liberica NIK because it supports AWT. Another NIK feature that you may find beneficial is ParallelGC, which is absent in the GraalVM Community version. At the Build stage, we run ./gradlew :plantuml-gplv2:nativeCompile -x test to generate a native image skipping the tests. At the Archive Release stage, we pack the native executable into a ZIP archive specifying the name for each platform. Finally, at the Upload binaries to release stage, we load the artifact for each operating system to the release. name: Native Image on: workflow_dispatch: push: branches: - master jobs: build_non_win_images: name: 'Build Native Image ${{ matrix.platform }}' strategy: matrix: os: [ macos-latest, windows-latest, ubuntu-latest ] include: - os: 'ubuntu-latest' platform: 'linux-amd64' - os: 'macos-latest' platform: 'darwin-arm64' - os: 'macos-13' platform: 'darwin-amd64' - os: 'windows-latest' platform: 'win-amd64' runs-on: ${{matrix.os}} steps: - name: Checkout the repository uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1 with: java-version: '21' github-token: ${{ secrets.GITHUB_TOKEN }} distribution: liberica cache: gradle - name: Build the GPLv2 plantuml run: | VERSION=$(grep 'version =' gradle.properties | cut -d' ' -f 3) echo VERSION $VERSION echo "VERSION=$VERSION" >> $GITHUB_ENV shell: bash - name: Build shell: bash run: | ./gradlew :plantuml-gplv2:nativeCompile -x test - name: Archive Release uses: thedoctor0/zip-release@0.7.5 with: type: 'zip' filename: "plantuml-${{ matrix.platform }}-${{ env.VERSION }}.zip" directory: plantuml-gplv2/build/native/nativeCompile/ - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: "plantuml-gplv2/build/native/nativeCompile/plantuml-${{ matrix. platform }}-${{ env.VERSION }}.zip" tag: ${{ env.VERSION }} overwrite: true make_latest: true That’s it! Now, if you push the project to the repository, the workflow will run automatically. Build Summary You can check the information about each build in the sidebar on the left: Build Stages And finally, the binaries for all platforms as well as the source code are located under the Releases: Resulting Binaries You can download the binary for your platform and verify that it is working. Conclusion In this article, we discussed how to set up a GitHub Action for building releases using GraalVM Native Image and examined several approaches to collecting metadata for the native-image tool. You can collect metadata manually, using the Tracing Agent, or a resource autodetection plugin that picks up the resources in specified directories. The choice, as usual, depends on your use case. - [Mastering Reactive Programming with Spring Data R2DBC](https://bell-sw.com/blog/mastering-reactive-programming-with-spring-data-r2dbc/): Spring Data R2DBC (Reactive Relational Database Connectivity) is part of the Spring Data project that enables the developers to build Spring applications combining relational databases and reactive programming. This article offers a guide to Spring Data R2DBC, starting with basic concepts and moving to more complex subjects as we go. We will Explore the core concepts of reactive programming and R2DBC, Offer a step-by-step tutorial for building your first project with Spring Data R2DBC, and Discuss more advanced topics such as differences between R2DBC and JDBC and nuances of working with R2DBC. If you already have an understanding of reactive programming and are familiar with the key concepts of R2DBC, feel free to jump to the section you are most interested in. The code for all examples is available on GitHub, the links will be provided in the corresponding sections. Table of Contents Introduction What is reactive programming What is Spring Data R2DBC Basics of Spring Data R2DBC Project setup: dependencies, database configuration Spring Data R2DBC Repositories Working with Flux and Mono REST API examples for creating, reading, updating, and deleting data Advanced Topics: Spring Data R2DBC vs Spring Data JDBC Embedded entities mapping Working with relationships Fluent API for DML operations Conclusion Introduction What is reactive programming Reactive programming is a declarative programming paradigm based on the concept of asynchronous (or non-blocking) event processing. It is aimed at a more efficient runtime resource usage when there’s some task in progress. In other words, a thread does not block or perform a busy wait for the environment to answer (e.g., waiting for the data on Unix domain or TCP/IP socket). This can be achieved by different means, for instance, via Linux epoll API (which, for example, Netty uses internally). As a result, reactive programming enables the creation of programs that can work with dynamic data flow. Reactive programming relies heavily on the Observer pattern and the concept of producers and consumers. The consumer registers itself in the producer's API to notify the latter that it wants to receive events from this stream (from now on, or all events). The producer notifies the consumers automatically of any state changes. Several consumers can subscribe to one publisher. Another important concept of reactive programming is reactive data streams. Reactive data stream is a flow of ordered-in-time data of undetermined volume, which is controlled in a push fashion. It means that a consumer doesn’t signal the producer that it is ready to accept events. It specifies the action to be taken when the event happens, and a producer is responsible for pushing the event onto all consumers that have subscribed. Reactive data streams can also be processed using different functions: you can aggregate, filter, map, merge streams, etc. On the whole, reactive programming doesn’t demonstrate better overall performance than its imperative counterpart, but reactive applications can potentially scale better under load. So, you can try it out with your application and see whether there are any benefits for your particular case. What is Spring Data R2DBC Developing a reactive app with the usual Spring Data APIs such as JDBC is challenging because of synchronous access to the database. So, the R2DBC specification was designed to provide the common set of features and interfaces that reactive drivers should have. Spring Data R2DBC abstracts these drivers away and provides some common Spring Data tools. Following database reactive drivers are currently supported: H2 (io.r2dbc:r2dbc-h2) MariaDB (org.mariadb:r2dbc-mariadb) Microsoft SQL Server (io.r2dbc:r2dbc-mssql) MySQL (io.asyncer:r2dbc-mysql) jasync-sql MySQL (com.github.jasync-sql:jasync-r2dbc-mysql) Postgres (io.r2dbc:r2dbc-postgresql) Oracle (com.oracle.database.r2dbc:oracle-r2dbc) Spring Data R2DBC is fairly similar to Spring Data JDBC, the reason being that the code base of the two is strongly interlaced. So if you are familiar with the latter, mastering R2DBC will be easier. However, there are some differences, some of them quite significant. Most of these differences are related to entity mapping. We will discuss these differences in more detail below. Differences aside (for a while), Spring Data R2DBC offers a selection of features for convenient database interaction, such as R2dbcEntityTemplate or Repository interfaces with support for custom queries. In the next section, we will see how we can implement CRUD operations with R2DBC API by creating a simple, yet fully functional demo application. Basics of Spring Data R2DBC Project setup: dependencies, database configuration The full code for the application below is available on GitHub. Prerequisites: Java 17+. You can use Liberica JDK recommended by Spring: download JDK 17 or 21 for your platform here or get it through your favorite package manager. Your favorite IDE. The best way to start a new Spring project is to visit Spring Initializr. We will need at least the following dependencies: Spring Data R2DBC PostgreSQL Driver Spring Reactive Web Liquibase Migration Generate the project and open it in your IDE. First of all, let’s configure the database connection. Although it is possible to configure Spring Data R2DBC via application.properties or yaml file when using it along the Spring Boot, we're going to configure the minimal required setup (like ConnectionFactory) manually. Create an ApplicationConfiguration class that extends AbstractR2dbcConfiguration, which contains essential bean declarations for the correct R2DBC functioning. The class requires three annotations: @Configuration, @EnableR2dbcRepositories (to be able to use reactive repository interfaces provided by Spring Data), and @EnableR2dbcAuditing (to enable auditing). We need to create a ConnectionFactory object in the ApplicationConfiguration class, which is similar to DataSource in JDBC. As we use PostgreSQL, we can create ConnectionFactory with the help of PostgresqlConnectionFactory by passing necessary parameters for database access. @Configuration(proxyBeanMethods = true) @EnableR2dbcRepositories @EnableR2dbcAuditing public class ApplicationConfiguration extends AbstractR2dbcConfiguration { @Value("${r2dbc.host}") private String host; @Value("${r2dbc.port}") private Integer port; @Value("${r2dbc.password}") private String password; @Value("${r2dbc.db}") private String database; @Value("${r2dbc.username}") private String username; @Bean @Override public ConnectionFactory connectionFactory() { return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() .host(host) .port(port) .username(username) .database(database) .password(password) .build() ); } @Bean public ReactiveTransactionManager reactiveTransactionManager() { return new R2dbcTransactionManager(connectionFactory()); } @Bean public TransactionalOperator transactionalOperator() { return TransactionalOperator.create(reactiveTransactionManager()); } } The application.properties file contains the following configurations. This application uses a locally installed PostgreSQL server. Note that R2DBC, just like JDBC, can’t create database schema based on the domain classes, so we need to provide a schema ourselves. The most conventional way is to use a Flyway or Liquibase migration tool. spring.application.name=r2dbc r2dbc.host=127.0.0.1 r2dbc.port=5432 r2dbc.db=customersdb r2dbc.username=postgres r2dbc.password=12345 spring.liquibase.url=jdbc:postgresql://localhost:5432/customersdb spring.liquibase.user=postgres spring.liquibase.password=12345 spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml logging.level.org.springframework.data.r2dbc=debug As we added the Liquibase dependency when creating the project, there’s already a db.changelog directory. So, the final touch would be to create a schema and populate our database with some sample data. The db.changelog-master.yaml file: databaseChangeLog: - include: file: db/changelog/changelog-1-schema.sql - include: file: db/changelog/changelog-2-data.sql The changelog-1-schema.sql file (the ALTER SEQUENCE part changes the current value of the id so that we can insert data without conflicts with the existing records): --liquibase formatted sql --changeset catherine:create-multiple-tables splitStatements:true endDelimiter:; CREATE TABLE customers ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL ); CREATE TABLE orders ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, customer_id BIGINT NOT NULL REFERENCES customers(id) ON DELETE CASCADE, price DOUBLE PRECISION NOT NULL, created_at TIMESTAMP NOT NULL ); ALTER SEQUENCE customers_id_seq RESTART WITH 101; ALTER SEQUENCE orders_id_seq RESTART WITH 101; The changelog-2-data.sql file: --liquibase formatted sql --changeset catherine:populate-tables-with-data splitStatements:true endDelimiter:; INSERT INTO customers (id, name, email) VALUES (1, 'Smith Paul', 'paul.smith@gmail.com'), (2, 'Doe Jane', 'jane.doe@gmail.com'); INSERT INTO orders (id, customer_id, price, created_at) VALUES (1, 1, 123.77, '2024-03-23T20:31:34.873822Z'), (2, 1, 20.5, '2024-06-10T15:06:34.873822Z'), (3, 2, 765.0, '2024-04-24T23:21:34.873822Z'); Finally, let’s create classes for our domain objects: Customer and Order. The Customer class: @Table(name = "customers") public class Customer { @Id private Long id; private String name; private String email; @Transient private List orders = new ArrayList<>(); public List getOrders() { return orders == null ? new ArrayList<>() : orders; } // getters, setters, constructors, equals and hashCode } The @Table annotation indicates that the class is a candidate for database mapping. The @Id annotation marks the primary key. What we should also note here is the @Transient annotation. As Spring Data R2DBC can’t automatically map relations, we should exclude these fields from being stored in the database. The Order class: @Table(name = "orders") public class Order { @Id private Long id; private Long customerId; private double price; @CreatedDate private Instant createdAt; // getters, setters, constructors, equals and hashCode } Here, we should note the auditing @CreatedDate annotation that declares the date the entity was created at. Finally, a CustomerDTO is a simple record class: public record CustomerDTO (Long id, String name, String email, List orders) { } Spring Data R2DBC Repositories The next step is to create repositories for our entities. There are several reactive repositories you can extend your repositories from. Their functionality is similar to that of the traditional Spring Data Repositories: ReactiveCrudRepository provides common methods for finding, saving, and deleting entities; ReactiveSortingRepository provides sorting capabilities for retrieving entities. In addition, there’s support for RxJava3CrudRepository and RxJava3SortingRepository that use RxJava 3 types. For example, let’s create repository interfaces for Customer and Order extending from ReactiveCrudRepository: public interface CustomerRepository extends ReactiveCrudRepository { } public interface OrderRepository extends ReactiveCrudRepository { Flux findAllByCustomerId(Long customerId); Mono deleteAllByCustomerId(Long customerId); } The key difference is that these interfaces enable us to handle database interaction in a reactive way using Flux and Mono provided by the Project Reactor. Before moving on, let’s look at these classes in more detail to understand what’s going on under the hood of R2DBC. Working with Flux and Mono Flux and Mono are types of a Publisher interface, which provides a stream of elements in response to the demand received from a Subscriber. A Publisher can have many Subscribers, but each Subscriber can subscribe only once to a single Publisher. After a Subscriber subscribes to a Publisher with subscribe(Subscriber), a Publisher can send one or several onNext(T t) notifications with data, onComplete() in the case of a successful terminal state, or onError(Throwable t) in the case of a failed terminal state. But the Publisher sends notifications only if Subscription.request(long) is invoked, where long is the number of elements a Subscriber is ready to accept. Now, returning to Flux and Mono. Mono emits 0 to 1 elements for the onNext() and completes with onComplete() or onError(). Flux, in turn, emits 0 to N elements for the onNext(), and after that completes successfully or with an error. We will see these classes in action in the following section. REST API examples for creating, reading, updating, and deleting data Let’s start with retrieving a customer by id. We will need a following method in our CustomerService class: public Mono findById(Long id) { return customerRepository.findById(id) .switchIfEmpty(Mono.error(new NoSuchElementException())) .flatMap(this::getOrdersForCustomer) .as(transactionalOperator::transactional); } The findById() method accesses the database and retrieves the entity if it is there. The switchIfEmpty() method is responsible for providing a fallback to another Mono in case this one completes without data. In our case, it creates a Mono that terminates with a specified error. In case we do have a Mono, we need to map it to a DTO and add a list of orders. That’s what flatMap() does: it transforms the value emitted by our Mono asynchronously and returns a value emitted by another Mono. We also need to add transaction management here and in all queries because if there are queries issued in different transactions, we could potentially break the consistency of the data. As we already know, R2DBC can’t automatically load the entity relations, so we need to do that manually. Let’s do that in a separate getOrdersForCustomer() method: private Mono getOrdersForCustomer(Customer customer) { return Mono.just(customer) .zipWith(orderRepository.findAllByCustomerId(customer.getId()).collectList()) .as(transactionalOperator::transactional) .map(result -> new CustomerDTO( result.getT1().getId(), result.getT1().getName(), result.getT1().getEmail(), result.getT2())); } What do we have here? just() creates a new Mono that emits a specified item, in our case, the customer; zipWith() combines the result of this mono with a new one (in this case, a List of orders for a given customer) into a Tuple2; map() enables us to use Tuple2 to create a CustomerDTO with all required fields, including a List of orders. The method for finding all customers is fairly similar, except that in this case, it returns a Flux of CustomerDTOs: public Flux findAll() { return customerRepository.findAll() .flatMap(this::getOrdersForCustomer) .as(transactionalOperator::transactional); } Let’s see how to create a customer: public Mono createCustomer(Customer customer) { if (customer.getId() != null) { return Mono.error(new IllegalArgumentException()); } return customerRepository.save(customer) .map(newCustomer -> new CustomerDTO( newCustomer.getId(), newCustomer.getName(), newCustomer.getEmail(), newCustomer.getOrders())) .as(transactionalOperator::transactional); } The method returns a Mono in case of a successful transaction, or a Mono that terminates with a specified error. If we want to update a customer, we should first check its existence in the database: private Mono customerExists(Long id) { return customerRepository.existsById(id).handle((exists, sink) -> { if (Boolean.FALSE.equals(exists)) { sink.error(new IllegalArgumentException()); } else { sink.next(exists); } }); } Let’s delve into what’s happening here. existsById() returns a Mono, which we then use in the handle() method that calls a BiConsumer with an output sink (SynchronousSink) for each onNext(). If the boolean equals false, we call SynchronousSink.error(Throwable). Otherwise, we call SynchronousSink.next(Object). After that, we can create a method for updating a customer and also for creating an order for a customer: public Mono updateCustomer(Customer customer) { return customerExists(customer.getId()) .then(customerRepository.save(customer)) .map(newCustomer -> new CustomerDTO( newCustomer.getId(), newCustomer.getName(), newCustomer.getEmail(), newCustomer.getOrders())) .as(transactionalOperator::transactional); } public Mono createOrderForCustomer(Order order) { if (order.getId() != null) { return Mono.error(new IllegalArgumentException()); } return customerExists(order.getCustomerId()) .then(orderRepository.save(order)).then() .as(transactionalOperator::transactional); } As for deleting a customer, we must first delete all orders associated with a given customer, and only after that we can delete a customer: public Mono deleteCustomer(Long id) { return orderRepository.deleteAllByCustomerId(id) .flatMap(c -> customerRepository.deleteById(id)) .as(transactionalOperator::transactional); } The CustomerController class is pretty straightforward and resembles the controllers we are used to writing (the full code can be found on GitHub): @RestController @RequestMapping("/customers") public class CustomerController { private final CustomerService customerService; public CustomerController(CustomerService customerService) { this.customerService = customerService; } @GetMapping("/{id}") @ResponseStatus(HttpStatus.OK) public Mono findCustomerById(@PathVariable Long id) { return customerService.findById(id); } @GetMapping @ResponseStatus(HttpStatus.OK) public Flux findAllCustomers() { return customerService.findAll(); } //other methods } Our first R2DBC application is ready! But the code above touches only the tip of an iceberg. The next section covers the intricacies of working with R2DBC in more complex scenarios. Advanced Topics: Spring Data R2DBC vs Spring Data JDBC As we stated, in terms of features and functionality, Spring Data R2DBC is not that much different from Spring Data JDBC. The reason for this is a significant overlap in their codebases (let alone the fact that all Spring Data modules have a common module inside). But still, there are some discrepancies. Let’s discuss them one by one. Embedded entities mapping Mapping of embedded entities is not supported in general in Spring Data R2DBC. It is not that it is quite difficult or impossible to implement, it is just not ready yet. Spring Data JDBC, on the other hand, can handle embedded entities, although there are some subtleties. Working with relationships Here, we need to pause for a moment. There is a problem: relationships in Spring Data R2DBC are not supported in principle. There has been a lot of discussion on this topic in various threads (here or here, for example). The very first question, absolutely legitimate: how did it happen? Why are they generally not supported and why was such a product released to the market at all? It is important to understand the reasoning behind. Let's start from the beginning, more precisely, from the process of loading entities. Eager relations Spring Data R2DBC does not have lazy loading in any form, just like Spring Data JDBC. There are some conceptual reasons for this. But in the case of R2DBC, the problem is not even conceptual; it is more related to the fact that we do not want to block when working in the world of reactive programming (otherwise, the whole point is lost). However, in this case, if we imagine that we have a getter, for example: Department getDepartment(); Then calling this method will block the thread when working with lazy loading — we simply have no choice. That is because the return value is the Department — a very concrete domain object. We cannot do it any other way. Someone might say that we can do it like this: Publisher getDepartment(); Or something similar. The answer is yes, theoretically, we can, but in this case, the data model becomes strongly tied to Reactor. The main question is — is this bad? There are many opinions on this, but the decision made by the Spring Data core team states that it is bad, and we will not do it this way. Another possible option would be to take an approach similar to Hibernate, that is, to proxy the returned entity. The problem is that in Hibernate, the boundaries and the consistency of lazy loading are strictly formed by the open session and a single transaction. But we do not have a notion of session in Spring Data R2DBC. Therefore, the only viable option now is to follow Spring Data JDBC path and work always in an eager loading mode. Loading of relationships It should be understood that it is not always possible to load an aggregate root along with its dependencies in one query with a couple of JOINs, for example. There are cases where such an approach breaks pagination, and the framework simply would be forced to send several different queries, like when dealing with the Cartesian product problem along with pagination. But the root of the problem here is that mapping data from the database to objects is essentially a synchronous process. If we send queries to the database at this stage, we get blocking behavior again. And there is simply no straightforward solution in this situation. Either we do not support relationships, or we block during mapping. But! It should be noted that overall, if there is an opportunity to fetch everything in one query, then in such a mode, support for relationships is theoretically possible. It's just that it would require writing quite a complex mechanism that currently does not exist. Fluent API for DML operations In conclusion, to lighten the mood, it is worth mentioning that Spring Data R2DBC supports a query builder API different from Spring Data JDBC. Examples can be found here. Overall, it's quite convenient: it’s still challenging to compete with Criteria API, but it's getting close. This API is implemented on top of the same R2dbcEntityTemplate. Conclusion To conclude, we need to emphasize the following points: Reactive applications in general do not perform as well as regular apps, but they tend to scale better under high load. JDBC API was initially designed to perform synchronous communication with RDBMS. R2DBC is a spec designed for drivers as an alternative way to interact with the database in an asynchronous way. Spring Data R2DBC is an even higher abstraction layer over the R2DBC compliant drivers that provides an easy way to work with the DB in a reactive manner. Spring Data R2DBC and Spring Data JDBC share a lot of features, mostly because of an overlap in their codebases. Spring Data R2DBC still lacks some functionality, part of which is problematic to implement from both conceptual and practical point of view. Still, Spring Data R2DBC is a promising project. Humanity is exploring ways to communicate reactively with R2BMS. As we saw, there are some challenges. For now, perhaps, reactive apps would work better with other databases, such as MongoDB, that do not have many of the problems described above. But that’s another story. - [Correctness in Java Concurrent Data Structures: What You Need to Know](https://bell-sw.com/blog/correctness-in-java-concurrent-data-structures-what-you-need-to-know/): In this short article, I will discuss the concept of linearizability and demonstrate that ConcurrentLinkedDeque in Java is non-linearizable by using Lincheck, a tool for testing concurrent data structures. You can find the code of the test on GitHub. Table of Contents What is Linearizability? Why Should You Care? ConcurrentLinkedDeque in Java is non-linearizable Conclusion What is Linearizability? Linearizability is a consistency condition for concurrent operations. It ensures that even though operations may be executed concurrently, they appear to take effect instantaneously at some point between their start and end time. Imagine you’re working with a shared object, like a queue or a stack. Linearizability ensures that every operation on this object looks as if it happened in a single, indivisible instant, even if behind the scenes, multiple threads are accessing it simultaneously. Why Should You Care? In the world of concurrent programming, achieving correctness is tricky. Without a clear consistency model, your code could behave unpredictably, leading to subtle bugs that are hard to reproduce and fix. Linearizability provides a strong guarantee that can simplify reasoning about your concurrent programs. Note that the absence of linearizability doesn’t imply that the data structures are incorrect. But it is an important property that you should take into consideration when building programs or libraries. ConcurrentLinkedDeque in Java is non-linearizable ConcurrentLinkedDeque is a concurrent deque based on linked nodes. You can insert and remove elements from both ends. In addition, it is thread-safe, which means that multiple threads can safely access, remove, and add elements to the queue. But ConcurrentLinkedDeque is non-linearizable, which was revealed by Lincheck, a framework for testing concurrent algorithms on JVM. Below is the code for the test written in Kotlin. The test uses Lincheck service annotations: class ConcurrentLinkedDequeTest { private val deque = ConcurrentLinkedDeque() @Operation fun addFirst(e: Int) = deque.addFirst(e) @Operation fun addLast(e: Int) = deque.addLast(1) @Operation fun pollFirst() = deque.pollFirst() @Operation fun pollLast() = deque.pollLast() @Operation fun peekFirst() = deque.peekFirst() @Operation fun peekLast() = deque.peekLast() @Test fun modelCheckingTest() = ModelCheckingOptions().check(this::class) } If we run the test, it “fails,” which means that the data structure is not linearizable. As you can see on the screenshot below, Lincheck provides not only the scenario on which ConcurrentLinkedDeque works incorrectly but also a detailed interleaving trace that leads to this error. First, we need to understand why this execution is non-linearizable. For peekLast() to return "1", it should happen before pollFirst(). In this case, addFirst(0) is also completed before pollFirst(), so pollFirst() should return "0"; however, it returns "1" on the diagram. But how does it happen? Let’s analyze the execution trace provided by Lincheck. The underlying data structure forms a doubly linked list, with head and tail pointers approximating its first and last nodes. Initially, "head" and "tail" point to a logically removed (Node.item == null) node. Then, addLast(1) is called, and the element “1” is added to the end of the linked list; “head’ and “tail” remain unchanged. After that, the pollFirst() operation starts, finding the first node with a non-null value. However, the thread gets preempted right before extracting the element “1” from “Node#1.” The execution switches to the second thread. The addFirst(0) operation adds “0” to the beginning of the linked list; “head” and “tail” still remain unchanged (and this is fine, just consider them oracles). Then, peekLast() reads the last element and returns “1”. Note that the concurrent pollFirst() is about to extract it, while "1" is no longer the first element. The pollFirst() operation atomically replaces “1” with “null” in “Node#2.item”, accidentally extracting an element that is no longer the first. The current implementation finds the first non-null node and extracts the element from it, but the whole world may change in between. Finally, the algorithm tries to unlink the extracted node from the linked list, but this part does not affect correctness. Conclusion There’s an issue in the JDK Bug System documenting this behavior of ConcurrentLinkedDeque. As the official documentation states: We believe (without full proof) that all single-element Deque operations that operate directly at the two ends of the Deque (e.g., addFirst, peekLast, pollLast) are linearizable (see Herlihy and Shavit's book). However, some combinations of operations are known not to be linearizable. In particular, when an addFirst(A) is racing with pollFirst() removing B, it is possible for an observer iterating over the elements to observe first [A B C] and then [A C], even though no interior removes are ever performed. Nevertheless, iterators behave reasonably, providing the "weakly consistent" guarantees. - [How to Perform Technology Risk Assessment at Small and Medium-sized Companies](https://bell-sw.com/blog/how-to-perform-technology-risk-assessment-at-small-and-medium-sized-companies/): If you’re running any business bigger than a coffee point, information technology is already a significant part of your company. Accounting software and payment solutions or marketing tools are some of the technologies you use to solve your business problems. For bigger companies, software is a no-brainer: modeling, logistics, invoicing, and many more areas are driven by information technology. For companies doing software in the first place, technology is at the very heart of the business: this is literally the foundation of everything. At the same time, there is no single business task with only one solution or vendor. Cloud providers, payment services, programming languages — there are always plenty of solutions. However, each choice is neither free nor safe: whatever you pick, you risk something. Going with managed services in the cloud means vendor-lock; using emerging technologies is subject to stability issues, and so on. Today, we are going to figure out how technology risk assessment works in companies of different scales. My name is Vladimir, I am a Senior Engineering Manager at Bolt leading Billing Teams in Commerce with a long track of software architecture and system design. I am ultimately accountable for technical decisions which my department makes. I have a relevant newsletter and a YouTube channel, so if you like the article, you can find more content there! Table of Contents Defining Technology Risk Assessment Risk Assessment in Startups and Small Companies Risk Assessment in Medium-sized Companies Conclusion Defining Technology Risk Assessment The common risk theory tells us that whatever choice we make brings risks with it. We can either accept, mitigate, or avoid the risk. However, in order to pick the risk-addressing strategy, we need to perform a risk assessment. This means listing the risks you see and understanding two characteristics of each one: likelihood and impact. Let’s say you pick up a database for a new project. One of the risks any database technology brings is the ability to scale beyond a point. The impact of such limitations will be staggering: you will need to pick a better option and migrate your data. The likelihood, on the other hand, is the probability you will have to do it at all, and it depends a lot on the business, data model, etc. Making this analysis results in mapping the risks on some risk matrix where you can pick up the appropriate actions. For example, you can try to avoid high risks (highly likely ones with high impact), accept the low risks (unlikely ones with low impact), and create mitigation strategies for moderate ones. Risk matrix Good! Now, let’s see how companies of different sizes will approach the technology risk assessment. Risk Assessment in Startups and Small Companies Early startups and small companies are characterized by limited budgets, smaller teams, and fewer resources. It typically means companies from single digits to a few dozen employees. Let’s look at a small startup in the compliance field: supplied.eu. Their goal is to provide Know-Your-Customer(KYC) and Know-Your-Business(KYB) services alongside Tax Reporting opportunities. Therefore, the key focus areas in the technology will be: Rapid Adaptation: Need for agility and quick pivoting to remain competitive. Vendor Lock-In: Risks associated with dependence on a single technology or vendor. Scalability Concerns: Ensuring that technology can grow with the company’s needs. Security: Balancing cost-effective security measures with the need for strong protection. Expertise: Having the necessary technical knowledge to use and develop solutions efficiently. The main aspects of core business are visual recognition and document parsing. Let’s try to make a technology risk assessment for the choice of technology. The options that we have as we need to extract information from the documents and recognize faces: Build the software ourselves using some open-source components; Find vendors for each component to support the required capabilities; Find a single vendor for the whole stack. Building our own solution To extract text from the documents, you need to have appropriate models, a pipeline to improve and deploy those models, and some infrastructure to feed the data into models for recognition. Thus, there are multiple risks associated with such an approach: High requirements for company staff and associated cost — remember the resource constraint. Long time-to-market: a startup should be agile and quick to try the ideas fast and understand what sticks. Reaching the goal in a reasonable time. For the sake of simplicity of the article, I will claim those risks are both highly probable and highly impactful. It means that you may have to close the business once any of those risks materialize. Find individual vendors So, imagine we went with individual vendors for text extraction capability and face detection and matching. For example, there is a Textract Service and Rekognition service at Amazon. Vendors like Abbyy offer that, and ChatGPT can read the documents, too. Let’s perform the risk assessment: The usage of a vendor will affect the cost of the service we provide. The vendor can be inflexible to our product demands. The vendor may be unreliable and unable to grant the required level of availability. Those risks are of medium probability, and their impact is pretty significant as well: either the prices will go up, or the quality of our service will suffer. However, we can implement efficient mitigation strategies here to use several vendors at once, make the quick failover, and negotiate the prices. Find a single vendor for the whole stack Relying on a single vendor brings its own risks, as well as the cost concerns and product demands. The biggest one is the vendor lock — if something goes really south, we are locked in with the company, and the transition will be painful and expensive. So again, it’s high impact but medium probability. Let’s bring all the risks into a table: Risk Factor Build the Software In-House Find Individual Vendors Find a Single Vendor for the Whole Stack Technical Expertise Required High Requires significant expertise and technical staff, which may be costly and difficult for a small startup. Medium Less in-house expertise required but needs skilled staff for integration and maintenance. Low Minimal in-house expertise needed; vendor handles most technical aspects. Time to Market High Risk Development from scratch is time-consuming, potentially delaying product launch. Medium Risk Integration of various vendors' services takes time but is faster than in-house development. Low Risk Fastest option, as the vendor provides a ready-to-use solution. Cost High Risk Significant initial investment in development, infrastructure, and ongoing maintenance. Medium Risk Service costs can add up, but initial costs are lower than in-house development. High Risk Potentially high ongoing costs and limited negotiation power over pricing. Flexibility/Adaptability High Flexibility Complete control over the solution, but requires time and resources to make changes. Medium Flexibility More flexible than a single vendor but can be limited by vendor APIs and SLAs. Low Flexibility Dependent on a single vendor's roadmap and update cycles. Vendor Lock-In None No dependency on external vendors, full control over the tech stack. Low to Medium Risk Limited vendor lock-in, as multiple vendors are used, but switching costs may still exist. High Risk Significant lock-in with a single vendor, making switching difficult and costly. Scalability Medium Risk Scalability depends on the quality of the in-house solution and the team's ability to handle growth. Medium Risk Scalability depends on vendors' ability to grow with your needs; multiple vendors might complicate this. Low to Medium Risk Scalability is managed by the vendor, but you're limited by the vendor's capabilities. Security High Risk Security is entirely the company's responsibility, requiring significant investment in securing the solution. Medium Risk Security partially managed by vendors, but in-house integration needs to ensure overall system security. Medium Risk Security is largely managed by the vendor, but there's a risk if the vendor's security fails. Reliability Medium Risk Reliability depends on the quality of the in-house solution and the team's ability to maintain it. Medium Risk Reliability depends on the reliability of multiple vendors; redundancy can mitigate risks. Medium Risk Reliability depends on the single vendor's infrastructure; failure can have a significant impact. Overall Risk Level High Multiple high-impact risks with high probabilities make this the riskiest option. Medium Balanced risk profile with some significant risks, but mitigation strategies exist. Medium to High High impact due to vendor lock-in, but medium probability makes it a slightly safer option than in-house development. As a result, we have three options. Two of them are in the High-Risk section of our risk assessment, so the decision here would be to pick several vendors for individual capabilities as it provides the most balanced approach. Risk Assessment in Medium-sized Companies The company grew and got some cash flow going. It now has more resources than startups, but still limited compared to large enterprises. At this point, technologies must support ongoing expansion and increased complexity. Therefore, the key focus areas will become: Integration Risk: Challenges in integrating new technologies with existing systems. Vendor Stability: Assessing the long-term viability of technology vendors. Regulatory Compliance: Ensuring new technologies meet industry standards and regulations. Data Security and Privacy: Stronger emphasis on protecting customer and business data. Talent Availability: Risks associated with the availability of skilled personnel for new technologies. Now let’s pick Bolt, an Estonian-based ride-hailing company. It managed to successfully expand to 50+ countries mainly in Europe and Africa; however, growth should continue. Let’s consider the decision about the compute layer. Bolt used to run all the payloads as Node.js services on the EC2 VMs in AWS but faced scalability, observability, and cost issues. In order to improve the situation, the company can consider the following options: Move payloads to serverless solutions, i.e. AWS Lambda; Move payloads to managed container orchestration solution like EKS/ECS; Migrate to self-managed Kubernetes cluster (EKS/OpenShift/etc). Let’s consider the risks for each of those picks. AWS Lambda AWS Lambda is a serverless offering that allows you to deploy a code archive or a container, and AWS handles the rest. You have limited options to control the layer: the memory limit, timeouts, etc. The risks associated with Lambda are the following: Cold Start Latency: AWS Lambda functions can experience cold start delays, especially for infrequently invoked functions or those using heavier runtimes. The increased latency can degrade user experience, particularly in time-sensitive applications. Limited Execution Time and Resource Allocation: Lambda imposes execution time limits (maximum 15 minutes) and memory/CPU resource constraints. It may not be suitable for long-running or resource-intensive tasks. Vendor Lock-In: Heavy reliance on AWS-specific services increases dependency on AWS and makes migration to another provider more complex. Reduced flexibility in the future if Bolt wishes to move to another cloud provider. Cost predictability: Costs can become unpredictable with Lambda, particularly with high volumes of invocations leading to Potential budget overruns if usage spikes. Managed Container Orchestration (EKS/ECS) All major clouds allow you to orchestrate containers with different offerings. Some of them are a fully managed Kubernetes environment, and some are simpler container managers like Elastic Container Service. However, the set of risks is similar: Management Complexity: Although managed, EKS/ECS still require considerable expertise to configure, manage, and optimize. Thus, you will have increased operational overhead and a potential learning curve for the development and SRE teams. Service Integration: Integrating existing services with a containerized environment may require significant refactoring, which increases development time and potential disruptions during migration. Resource Over-Provisioning: Inefficient container orchestration can lead to over-provisioning of resources, increasing costs. Availability and SLAs: While managed, EKS/ECS services are subject to AWS SLAs, which may not meet internal availability requirements. Migrating to a Self-Managed Cluster (Kubernetes/OpenShift/etc.) Risks: Operational Overhead: Self-managing a Kubernetes cluster involves significant complexity, including maintaining control plane components, networking, security, and upgrades, imposing a high operational burden and potential for human error. Security Management: Self-managing Kubernetes increases the responsibility for security patches, updates, and configuration management, which adds up to the probability of potential security vulnerabilities if patches or configurations are not managed correctly. Resource Allocation and Costs: Inefficient resource management can lead to over-provisioning or underutilization of resources. Disaster Recovery and High Availability: Ensuring high availability and a robust disaster recovery plan is more complex in a self-managed environment. Talent and Expertise Requirements: Managing a Kubernetes cluster requires a specialized skill set that may not currently exist within the team. Conducting the risk analysis for these options, we can say that cold start latency, limited execution time, and vendor lock-in are both high risk and high probability, so, we eliminate this option, balancing out self-managed vs managed Kubernetes. This situation is pretty tough: the sets of risks are both medium severity and medium probability. If I were to make such a decision, I would say that security management, disaster recovery, and talent requirements have higher risks as well as higher impact, so I would go with EKS. However, in your company, you can assess the risks differently based on your situation, access to talent, and other factors. Conclusion No technology is perfect, and each choice brings its own set of pros and cons alongside the associated risks. In order to make a calculated decision, you must conduct the risk assessment and estimate the impact and probability of each risk, weighing them all out. I wish you solid decisions! - [A guide to pattern matching for switch in Java 21](https://bell-sw.com/blog/a-guide-to-pattern-matching-for-switch-in-java-21/): Pattern matching for switch statements and expressions was introduced in JDK 17 and refined in the following releases. This feature evolved together with record patterns included into JDK 19. Both features were finalized in Java 21, so let’s see how we can use them in development. The article contains some theory on switch statements / expressions and pattern matching, so if you are already familiar with the concepts, feel free to jump to the practical section. Table of Contents What are switch statements and expressions What is pattern matching How to use pattern matching for switch Patterns in case labels Pattern matching with the null case label Guarded pattern case labels and a when clause How to order case labels Exhaustiveness of switch statements and expressions Using record patterns with switch Conclusion What are switch statements and expressions The switch statements are statements with multiple execution paths. Using switch instead of traditional if-else statements helps to significantly reduce boilerplate code. So, for instance, instead of this code: String dayOfWeek = "Tuesday"; String moodOfTheDay; if (dayOfWeek.equals("Monday")) { moodOfTheDay = "Ready to conquer the world"; } else if (dayOfWeek.equals("Tuesday")) { moodOfTheDay = "In desperate need of a chocolate cake"; } else if (dayOfWeek.equals("Wednesday")) { moodOfTheDay = "Exhausted"; } else if (dayOfWeek.equals("Thursday")) { moodOfTheDay = "Exhausted"; } else if (dayOfWeek.equals("Friday")) { moodOfTheDay = "Party hard"; } else if (dayOfWeek.equals("Saturday")) { moodOfTheDay = "Where am I?"; } else if (dayOfWeek.equals("Sunday")) { moodOfTheDay = "Watching the clouds pass by"; } else { moodOfTheDay = "There's no such day"; } System.out.println(moodOfTheDay); We can write this: String dayOfWeek = "Tuesday"; String moodOfTheDay; switch (dayOfWeek) { case "Monday": moodOfTheDay = "Ready to conquer the world"; break; case "Tuesday": moodOfTheDay = "In desperate need of a chocolate cake"; break; case "Wednesday": moodOfTheDay = "Exhausted"; break; case "Thursday": moodOfTheDay = "Exhausted"; break; case "Friday": moodOfTheDay = "Party hard"; break; case "Saturday": moodOfTheDay = "Where am I?"; break; case "Sunday": moodOfTheDay = "Watching the clouds pass by"; break; default: moodOfTheDay = "There's no such day"; break; } System.out.println(moodOfTheDay); A switch statement consists of a body in braces (switch block), which includes one or multiple labels followed by statements (code to be executed): the case labels containing values to which the switch expression is compared, and an optional default label, whose statement is executed in case the switch expression doesn’t match any value in the case labels. The example above is the traditional fall-through switch statement with a case …: label. Fall-through means that all switch statements will be executed unless there’s a break clause in each. This semantics has long been an irritation point for developers as it is verbose and error-prone. So, Java 14 brought a new case … -> label that 1. Means that only one statement after the label matching the expression will be executed even without the break clause, and 2. Allows for multiple constants per case. Therefore, the code above can be simplified to: String dayOfWeek = "Tuesday"; String moodOfTheDay; switch (dayOfWeek) { case "Monday" -> moodOfTheDay = "Ready to conquer the world"; case "Tuesday" -> moodOfTheDay = "In desperate need of a chocolate cake"; case "Wednesday", "Thursday" -> moodOfTheDay = "Exhausted"; case "Friday" -> moodOfTheDay = "Party hard"; case "Saturday" -> moodOfTheDay = "Where am I?"; case "Sunday" -> moodOfTheDay = "Watching the clouds pass by"; default -> moodOfTheDay = "There's no such day"; } System.out.println(moodOfTheDay); Java 14 also introduced switch expressions that assigns a value to the given variable directly. So, we can simplify our example even more: String dayOfWeek = "Tuesday"; String moodOfTheDay = switch (dayOfWeek) { case "Monday" -> "Ready to conquer the world"; case "Tuesday" -> "In desperate need of a chocolate cake"; case "Wednesday", "Thursday" -> "Exhausted"; case "Friday" -> "Party hard"; case "Saturday" -> "Where am I?"; case "Sunday" -> "Watching the clouds pass by"; default -> "There's no such day"; }; System.out.println(moodOfTheDay); But despite the fact that switch expressions became more developer-friendly, they still possessed some limitations: Support for a limited number of types: primitive types (int, byte, char, short, except for long), their boxed forms (Integer, Byte, Short, Character), enums, and String; Special treatment of null values: separate code block outside of a switch statement / expression for handling nulls is required; The switch expression can be tested against constants in case labels only for exact equality. This is where pattern matching comes to rescue. What is pattern matching Pattern matching means testing an object against a particular structure and then extracting values from the object if it matches some structure. A pattern consists of a test applied to the target (the object that we check) and a set of pattern variables, which are extracted from the target only if the test passes successfully. Pattern matching has already been used in regular expressions. But this feature was extended to the instanceof operator in JEP 394 for Java 16. Thanks to pattern matching for instanceof, instead of introducing a local variable, assigning the given expression, casting it to specific type, and only then processing the expression like this: Object myObject = "This is a String"; if (myObject instanceof String) { String s = (String) myObject; System.out.println(s.toUpperCase()); } We can simply write this: Object myObject = "This is a String"; if (myObject instanceof String s) { System.out.println(s.toUpperCase()); } Where String s is a type pattern, which is a pattern that takes type as a test and contains one pattern variable, to which the target is assigned. In further JDK releases, the scope of pattern matching was extended to switch statements / expressions to overcome their limitations and avoid boilerplate with multiple if-else statements. How to use pattern matching for switch Prerequisites: JDK 21. You can use Liberica JDK 21 for your platform by getting it directly from the website or using your favorite package manager. IDE of choice. Patterns in case labels The syntax of a switch statement / expression doesn’t change, only now we use type patterns in case labels, plus we can use any type to test against as opposed to the traditional switch: static String patternTypes(Object myObject) { return switch (myObject) { case Integer i -> "This is an Integer: " + i; case Long l -> "This is a Long: " + l; case Dog d -> "This is a dog that makes " + d.voice(); case ArrayList a -> "This is an ArrayList of size " + a.size(); default -> "This is some other object: " + myObject.getClass(); }; } public static class Dog { String voice = "Woof!"; public String voice() { return voice; } } Pattern matching with the null case label What about the null values? How can we handle them? There’s no need to carry the logic of testing against null outside the switch block, we can now do that inside the block using a case null label: static String patternTypes(Object myObject) { return switch (myObject) { case Integer i -> "This is an Integer: " + i; case Long l -> "This is a Long: " + l; case Dog d -> "This is a dog that makes " + d.voice(); case ArrayList a -> "This is an ArrayList of size " + a.size(); case null -> "You gave me a null value"; default -> "This is some other object: " + myObject.getClass(); }; } Note that if you don’t add the null case, switch will throw a NullPointerException like it always has. Guarded pattern case labels and a when clause What if we need to further test the value when it matches some pattern? For instance, we want to accept only Integers of certain values. One way is to use nested if-else statements, but it leads to redundant and potentially error-prone code. Luckily, we can use guarded pattern case labels that contain a type pattern and a boolean expression or guard. static String guardedPatterns (Object myObject) { return switch (myObject) { case Integer i when i == 1 -> "Connecting you to the Manager..."; case Integer i when i == 2 -> "Connecting you to the Sales Department..."; default -> "Invalid input"; }; } How to order case labels Let’s linger on the previous example for a while. We can add a condition that checks whether the given object is an Integer of any value rather than 1 or 2: static String guardedPatterns (Object myObject) { return switch (myObject) { case Integer i when i == 1 -> "Connecting you to the Manager..."; case Integer i when i == 2 -> "Connecting you to the Sales Department..."; case Integer i -> "Invalid number"; default -> "Totally invalid input!"; }; } The code functions correctly, but what if we change the case order and put case Integer i -> on top? static String guardedPatterns (Object myObject) { return switch (myObject) { case Integer i -> "Invalid number"; case Integer i when i == 1 -> "Connecting you to the Manager..."; case Integer i when i == 2 -> "Connecting you to the Sales Department..."; default -> "Totally invalid input!"; }; } In this case, you will get a compile-time error java: this case label is dominated by a preceding case label. What went wrong? If we place case Integer i -> before case Integer i when i == 1, the first pattern case will dominate the second pattern case, meaning that any Integer matches the more general condition, and so the switch won’t go any further to analyze more specific cases and execute this statement. Therefore, A more general (unguarded) pattern case label dominates the guarded case label with the same condition; A pattern case label dominates the constant case label, A type of a pattern case label dominates the subtype of a pattern case label. So, for instance, placing a parent class in a pattern case over a child class like that static String patternOrder(Object myObject) { return switch (myObject) { case Animal a -> "This is an animal that makes " + a.voice(); case Dog d -> "This is a dog that makes " + d.voice(); default -> "This is not an animal"; }; } public static class Animal { String voice = "a random sound"; public String voice() { return voice; } } public static class Dog extends Animal { String voice = "woof!"; public String voice() { return voice; } } Will also result in a compile-time error. To conclude this section, there’s a simple rule to ordering pattern case labels: move down from more specific to more general conditions. Constant case labels should come first, then guarded pattern case labels, then unguarded pattern case labels. Subtypes should precede types. Exhaustiveness of switch statements and expressions Exhaustiveness of switch statements / expressions implies that all possible values of the selector expression must be handled in the switch block. Let’s take one of the snippets we created above. If we remove the default case like that static String patternTypes(Object myObject) { return switch (myObject) { case Integer i -> "This is an Integer: " + i; case Dog d -> "This is a dog that makes " + d.voice(); case ArrayList a -> "This is an ArrayList of size " + a.size(); }; } it will result in compile-time error java: the switch expression does not cover all possible input values. This functionality is especially useful with enums and sealed classes. Consider the following enum class: enum DeliveryStatus { IN_TRANSIT, DELIVERED, CANCELED } If we want to use a switch statement / expression with our enum, we can specify all possible input values without the default clause, and leave exhaustiveness checks to the compiler. static String exhaustiveSwitch(DeliveryStatus status) { return switch(status) { case IN_TRANSIT -> "Order is on its way"; case DELIVERED -> "Order successfully delivered"; case CANCELED -> "Order canceled"; }; } Removing one of the case labels will lead to a compile-time error. There’s logic to that. Suppose we extend our enum with additional values. Without the requirement for exhaustiveness, we could get away with not adding new values to switch, which will result in undesired application behavior. So, if you add new constants to your enum class without recompiling the class containing the switch expression, the compiler will throw an exception. The same applies to sealed classes and interfaces that restrict what other classes or interfaces can extend or implement them. For example, we have a sealed class Vehicle that lets only Motorcycle, Car, and Van extend it: public static abstract sealed class Vehicle permits Motorcycle, Car, Van { int maxSpeed; double maxPermittedWeight; abstract String printInfo(); } public static final class Motorcycle extends Vehicle { int maxSpeed = 140; double maxPermittedWeight = 350.5; @Override String printInfo() { return "max speed: " + maxSpeed + "\nmax permitted weight: " + maxPermittedWeight; } } public static non-sealed class Car extends Vehicle { final int maxSpeed = 137; final double maxPermittedWeight = 850.0; final String fuel = "petrol"; @Override String printInfo() { return "max speed: " + maxSpeed + "\nmax permitted weight: " + maxPermittedWeight + "\nfuel: " + fuel; } } public static final class Van extends Vehicle { int maxSpeed = 111; double maxPermittedWeight = 1543.0; final int passengerSeats = 10; @Override String printInfo() { return "max speed: " + maxSpeed + "\nmax permitted weight: " + maxPermittedWeight + "\npassenger seats: " + passengerSeats; } } We now have to cover all classes permitted by Vehicle in our switch expression. static String exhaustiveSwitchWithSealed(Vehicle vehicle) { return switch (vehicle) { case Motorcycle m -> m.printInfo(); case Car c -> c.printInfo(); case Van v -> v.printInfo(); }; } But there’s a catch. If you remove one of the input values (here or in the enum example), and instead, add a default clause like that static String exhaustiveSwitchWithSealed(Vehicle vehicle) { return switch (vehicle) { case Motorcycle m -> m.printInfo(); case Car c -> c.printInfo(); default -> "No such vehicle"; }; } The code will compile and run without errors. So, the general recommendation would be to avoid using default clauses in switch statements / expressions working with enums and sealed classes. Before we move on to another section, I would like to highlight another possible pitfall related to enums in switch. If you use a traditional switch statement with case …: labels, and don’t specify all existing enum constants: static String nonExhaustiveSwitch(DeliveryStatus status) { String response = ""; switch (status) { case IN_TRANSIT: response = "Order is on its way"; break; case DELIVERED: response = "Order successfully delivered"; break; } return response; } The code will also compile and run without errors. In case you use Intellij IDEA, Nicolai Parlog offered a workaround to this issue. Go to Settings -> Editor -> Inspections -> Enum ‘switch’ statement that misses case, and set the severity level to Error. Apply the changes. After that, you will see that the incomplete switch statement is highlighted by the IDE. Using record patterns with switch Record patterns combine the power of Java records (classes that act as immutable data carriers) and pattern matching. A record pattern includes a record class type and a pattern list matched against the record component values. For instance, we have a record Rectangle: record Rectangle(double a, double b) { } Then the corresponding record pattern will be Rectangle(double a, double b). Now, we can use this record pattern with the instanceof operator: without creating local variables and extracting the components explicitly, we can access the value components directly: static double getArea(Object shape) { if (shape instanceof Rectangle(double a, double b)) { return a * b; } else throw new IllegalArgumentException("Unknown shape"); } In a simple case without complex hierarchies, record patterns in switch statements / expressions help us to write very laconic code: static double switchWithRecordPattern(Object shape) { return switch (shape) { case Rectangle(double a, double b) -> a + b; case Square(double a) -> a * a; default -> throw new IllegalStateException("Unexpected value: " + shape); }; } record Rectangle(double a, double b) { } record Square(double a) { } In case you have a more complex situation with hierarchy involved: class Tesla {} class ModelY extends Tesla {} record Selection (T first, T second) { } You must make sure that the switch statement / expression is exhaustive, i.e., all possible combinations of the component pattern types must be covered: static String switchRecordExhaustive(Selection car) { return switch (car) { case Selection (ModelY modelY1, ModelY modelY2) -> "ModelY and ModelY"; case Selection (Tesla tesla, ModelY modelY) -> "Tesla and ModelY"; case Selection (ModelY modelY, Tesla tesla) -> "ModelY and Tesla"; case Selection (Tesla tesla1, Tesla tesla2) -> "Tesla and Tesla"; }; } Conclusion Pattern matching for switch is a useful feature that makes the code more concise, expressive, and reliable. Subscribe to our newsletter if you want to learn about other cool features in newer Java versions! - [Liberica JDK 23 is released](https://bell-sw.com/blog/liberica-jdk-23-is-released/): We are happy to announce that Liberica JDK 23 builds are generally available! The release contains 2,548 fixes overall — 2,388 in JDK and 160 in FX. The BellSoft engineers resolved 8 issues. 12 Java Enhancement Proposals (JEPs) with new, enhanced, or deprecated functionality. Summary of JEPs in JDK 23 New features facilitating development: JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) allows for the usage of both reference and primitive types in record patterns and pattern matching for instanceof and switch. JEP 467: Markdown Documentation Comments enables the developers to write javadoc documentation comments in Markdown, although it is still possible to use the HTML elements and JavaDoc @-tags. JEP 476: Module Import Declarations (Preview) makes it possible to implicitly import all packages exported by a module. Improved features enhancing performance and maintainability of Java programs: JEP 466: Class-File API (Second Preview) introduces a standard API for working with Java class files. JEP 469: Vector API (Eighth Incubator) provides an API for writing vector algorithms that compile reliably at runtime to optimal vector instructions. JEP 473: Stream Gatherers (Second Preview) adds the support for custom intermediate operations to Stream API. JEP 474: ZGC: Generational Mode by Default deprecates non-generational mode of Z Garbage Collector, making generational mode default. JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview) makes it easier to learn Java by enabling the developers to write simple single-class programs without using complex features and concepts. JEP 480: Structured Concurrency (Third Preview) improves reliability and observability of multithreaded code by treating a task and related subtasks as a single unit of work belonging to one syntactic block. JEP 481: Scoped Values (Third Preview) provides a more reliable way of sharing data between methods than thread-local variables by introducing scoped values for sharing immutable data without using method parameters. JEP 482: Flexible Constructor Bodies (Second Preview) allows the developers to use certain statements in constructors before the explicit constructor invocation. Deprecated features increasing code safety: JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal deprecates methods previously used to perform low-level operations, encouraging the developers to use more reliable and performant APIs, Variable Handles and Foreign Function & Memory API. You can read more about each JEP in our Overview of JDK 23 features. Download Liberica JDK 23 builds now! Regardless of whether you are planning to migrate to the next LTS release or just want to play around with new Java features, you can download and use Liberica JDK for free. Head over to the Liberica JDK Download Center to get the fresh JDK builds for your platform. - [Liberica Native Image Kit 24.1.0 is released](https://bell-sw.com/blog/liberica-native-image-kit-24-1-0-is-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 24.1.0 for JDK 23. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable new features and enhancements Liberica Native Image Kit is based on GraalVM Community Edition and includes many improvements, including: A new optimization level -Os was added, which enables you to get a smaller native image. Reachability metadata is now collected into a single reachability-metadata.json file instead of separate reflection-config.json, resource-config.json, etc. files. Creation of separate .json files is deprecated but still accepted. SerialGC now performs compact garbage collection for the old generation (enabled with -H:+CompactingOldGen), which reduces memory consumption. A new --static-nolibc option was introduced to build mostly static native images. Static native executables can be deployed in distroless containers for additional container image size reduction. You can find a more detailed description of these and other changes in the Release Notes. Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [What is CI/CD](https://bell-sw.com/blog/what-is-ci-cd/): Continuous Integration and Continuous Delivery/Deployment (CI/CD) are a set of practices aiming to streamline the processes of developing, testing, and releasing/deploying a software product. CI/CD enables companies to move away from a waterfall way of releasing software once in an established period of time to introducing changes in a more agile way, thus helping the developers to roll out new functionality in incremental steps and receive feedback from the users faster. But what exactly are CI and CD? What kind of processes are they built upon? Do you really need CI/CD at your company? How do you build a reliable CI/CD pipeline? Find the answers in this article! Table of Contents What is Continuous Integration? What is Continuous Delivery? What is Continuous Deployment? CI/CD pipeline architecture Why is CI/CD important? CI/CD and DevOps Common CI/CD tools Conclusion What is Continuous Integration? Continuous Integration (CI) is aimed at providing users with new functionality as soon as possible and get instant feedback. It implies that developers commit code changes frequently, which are then merged with the main development branch and delivered to production. For instance, you want to add a new feature to your product, whose development will take several weeks or even months. You can spend all that time developing the feature only to find out after rolling it out that the users don’t like it! Alternatively, you can roll out the changes incrementally, see how users react, study their feedback, and apply changes promptly. This way, you will save money on developing functionality users don’t want and at the same time, will be able to provide users with reliable and high-quality software answering their needs. CI implies frequent code commits: usually, once a day, but it can be several times a day. Why is it important to commit code continuously? Introduction of small changes helps to notice bugs at early development changes, saving developers time on debugging. After developers have committed their changes, the project build is triggered. If successful, the build is run through numerous unit and integration tests to verify that the change hasn’t broken anything. If any of the tests fail, the committed code is rejected. Otherwise, the change is merged into the main branch, and the artifact is built (it can be a Docker image, for instance). To sum up, continuous integration includes the following stages: code committing, project building, testing, and artifact building. What is Continuous Delivery? Continuous Delivery (CD) implies that the code is staged for production as soon as the changes are integrated into the main project branch. CD further automates processes of software releases. To illustrate: building a Docker image is part of continuous integration, deploying the image to container registry is continuous delivery provided that it happens automatically. To send code to production, the operations engineers only need to push the button. CD is a good fit for canary deployments and blue-green deployments. Canary deployments mean that you roll out product updates to a small portion of users; for instance, you deploy the changes to 1% of your nodes. In case of positive feedback from the users and no crashes, you roll out the update to 10% of nodes, then 50%, and so on, with the goal of providing the update to all users. But in case something goes wrong on the first stages, you can quickly roll back the update, redirecting the traffic to healthy nodes. Blue-green deployments are a strategy of introducing changes to the application with potentially zero downtime. In this scenario, you divide your runtime environment in halves: one of them (blue) serves traffic, the other one doesn’t handle any requests and can be accessed through a private network only. When you need to update the application, you deploy changes to the green environment, where they are thoroughly tested. If everything works fine, the traffic is directed to the green environment. Whichever strategy you choose, established CD processes will help you achieve deployment consistency and reliability. What is Continuous Deployment? The term Continuous Deployment is sometimes used interchangeably with Continuous Delivery. It would even be safe to assume that in most cases when CI/CD is implemented at a company, CD already includes delivery and deployment. But sometimes, these processes need to be divided. Continuous Deployment means that the code is automatically pushed to production. Therefore, it implies full automation of the processes between committing the code and making the update available to the end users. On the one hand, this practice takes the burden of manual deployment off the operation team. On the other hand, it can be integrated only when you have robust testing processes minimizing the risks of mayhem on production. CI/CD pipeline architecture Summarizing the key points from sections above, the CI/CD pipeline can be depicted as follows: CI/CD pipeline CI can exist without CD. In very rare situations, CD can also exist without CI. Furthermore, the simplest CI pipeline can be based on bash solely. For instance, you can set up specific server hooks that trigger custom logic every time developers do git push. Then, you can add additional tools, most common of which are summarized below. Why is CI/CD important? You don’t have to implement CI/CD, but you will most certainly benefit from it. Here’s why: CI/CD helps to glue together the processes of development, testing, and deployment into one smooth pipeline to reduce software release cycles without losing quality of software. It is possible to detect bugs breaking the project and performance regression earlier, meaning that it is easier for developers to find and fix the issues. New features are available to users faster. In the case of websites, the changes may go live in a few minutes after committing the code. In the absence of a “big deployment day,” when all changes accumulated during an extended period of time go to production, there are no stressed out, overworking developers trying to fix the unexpected bug the night before the release. CI/CD enables you to automate repetitive processes such as testing and free the time of developers, testers, and operations engineers. CI/CD and DevOps CI/CD is part of the DevOps methodology that aims to build a bridge between development and operations teams. By automating the processes of building, testing, and deploying code, CI/CD facilitates the work of developers that can commit code faster and operations engineers that want to have a stable and standardized delivery process. DevOps, in turn, originates from Agile software development methodology whose Manifesto places priority on collaboration between teams and customers and rapid responsiveness to changes. CI/CD helps to put these priorities to life by enabling the developers to deliver code changes faster. Common CI/CD tools As mentioned above, you can build a Continuous Integration pipeline with bash only. However, if you want to have a convenient user interface and introduce more complex processes such as monitoring, security checks, and so on, you might want to use the solutions developed specifically for implementing CI/CD. Some common solutions include: Jenkins is an open-source framework for automating all sorts of processes related to building, testing, and deploying software. It is a Java-based application with a user interface that offers hundreds of plugins enabling you to perform any task or connect to a wide variety of tools. GitHub Actions enables you to automate build, test, and deploy processes right from the Github repository. It is possible to write your own actions by describing required workflows in separate YAML files or load existing Actions from the Marketplace. For instance, you can install Java such as Liberica JDK by using the setup-java action or generate native images using the GitHub Action for GraalVM. A tutorial on creating native images with a Github action can be found here. GitLab CI/CD helps you to set up a CI/CD pipeline within GitLab. Pipeline stages and jobs to be performed at these stages are defined in the .gitlab-ci.yml file. In addition, GitLab CI/CD offers Auto DevOps, which is a set of pre-configured integrations similar to GitHub Actions. But these platforms are only a drop in the ocean. You can find a more detailed overview of these and a variety of other popular technologies in the Overview of CI/CD tools. If you deploy your application to the cloud, container registries such as Docker and container orchestrators such as Kubernetes also become part of your CI/CD pipeline. In this regard, using smaller container images will enable you to accelerate push / pull time. Consider using base images with Alpaquita Linux, 100% compatible with Alpine, but with two flavors based on musl and glibc and increased security including LTS releases and enterprise support. Speaking of security: ensuring the security of your code becomes even more vital when implementing CI/CD because software supply chains are increasingly becoming targets of hacker attacks. In this case, reducing the CI/CD security risks defined by OWASP is strongly recommended. In addition, you should make sure that your project is free of known vulnerabilities, and that developers don’t introduce vulnerabilities with their changs. For that purpose, you can integrate vulnerability scanners into your pipeline. And if you have a Java project, using Liberica JDK and Alpaquita Linux provided with a Software Bill of Materials will enable you to set up SBOM validation within the pipeline. Conclusion Adopting CI/CD will help you shorten software release cycles and provide value to users faster. In turn, your teams will have more time working on adding value to the product thanks to automation of many processes. - [How to use buildpacks with Spring Boot](https://bell-sw.com/blog/how-to-use-buildpacks-with-spring-boot/): Buildpacks let you containerize a Spring Boot application without writing or maintaining a Dockerfile or even installing additional tools. In this guide, we will build a production-ready container image using Spring Boot’s Maven or Gradle support, run it locally, and explore options for customizing the Java runtime, JVM settings, startup optimization, and image size. We’ll also cover creating native images with buildpacks. Table of Contents Set up the project Create a Docker image using buildpacks Run the Docker image Create a native image using buildpacks Customize a buildpack Choose a Java version Choose another builder Configure the JVM Enable CDS and AOT for faster startup Conclusion Prerequisites: A Spring Boot application. You can take your own app, pull the reference Spring Boot Petclinic app from GitHub, or create a demo project with Spring Initializr; JDK 21. You can download Liberica JDK recommended by Spring and used by default in Paketo buildpacks for Spring; Your favorite IDE; Docker. Set up the project I’m going to use a simple demo application generated with Spring Initializr. I added only one dependency, Spring Web, and created a @RestController class with one method: @RestController public class WelcomeController { @GetMapping("/") public String showGreeting() { return "Hello! We love Paketo buildpacks!"; } } Create a Docker image using buildpacks If you created a new Spring Boot project or took an existing one, it already contains a Maven / Gradle plugin responsible for implementing buildpacks. Maven: org.springframework.boot spring-boot-maven-plugin Gradle: plugins { java id("org.springframework.boot") version "3.3.4" id("io.spring.dependency-management") version "1.1.6" } To containerize the application with default settings, you don’t need to configure anything. Therefore, make sure that JAVA_HOME is set to the Home directory of your installed Java 21 distribution and run the following command. For Maven: mvn spring-boot:build-image For Gradle: gradle bootBuildImage It will take about two minutes to generate the image; meanwhile, you can study the terminal output to understand what is going on under the hood. For instance, you can see the Ubuntu Jammy base is used as a base OS image: [INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder-jammy-base:latest' 100% ... [INFO] > Pulling run image 'docker.io/paketobuildpacks/run-jammy-base:latest' 100% Then, you can see which buildpacks are participating in the build: [INFO] [creator] 6 of 26 buildpacks participating [INFO] [creator] paketo-buildpacks/ca-certificates 3.8.6 [INFO] [creator] paketo-buildpacks/bellsoft-liberica 10.8.4 [INFO] [creator] paketo-buildpacks/syft 2.1.0 [INFO] [creator] paketo-buildpacks/executable-jar 6.11.2 [INFO] [creator] paketo-buildpacks/dist-zip 5.8.4 [INFO] [creator] paketo-buildpacks/spring-boot 5.31.2 Buildpacks use a build-time base image for creating a build environment, and a runtime image for creating the runtime environment for the application. A Java runtime is used at both stages. By default, Paketo buildpacks use Liberica JDK that provides the following configurations (I will show you how to work with them later in this tutorial): JVM Build Configuration [INFO] [creator] Paketo Buildpack for BellSoft Liberica 10.8.4 [INFO] [creator] https://github.com/paketo-buildpacks/bellsoft-liberica [INFO] [creator] Build Configuration: [INFO] [creator] $BP_JVM_JLINK_ARGS --no-man-pages --no-header-files --strip-debug --compress=1 configure custom link arguments (--output must be omitted) [INFO] [creator] $BP_JVM_JLINK_ENABLED false enables running jlink tool to generate custom JRE [INFO] [creator] $BP_JVM_TYPE JRE the JVM type - JDK or JRE [INFO] [creator] $BP_JVM_VERSION 17 the Java version [INFO] [creator] Launch Configuration: [INFO] [creator] $BPL_DEBUG_ENABLED false enables Java remote debugging support [INFO] [creator] $BPL_DEBUG_PORT 8000 configure the remote debugging port [INFO] [creator] $BPL_DEBUG_SUSPEND false configure whether to suspend execution until a debugger has attached [INFO] [creator] $BPL_HEAP_DUMP_PATH write heap dumps on error to this path [INFO] [creator] $BPL_JAVA_NMT_ENABLED true enables Java Native Memory Tracking (NMT) [INFO] [creator] $BPL_JAVA_NMT_LEVEL summary configure level of NMT, summary or detail [INFO] [creator] $BPL_JFR_ARGS configure custom Java Flight Recording (JFR) arguments [INFO] [creator] $BPL_JFR_ENABLED false enables Java Flight Recording (JFR) [INFO] [creator] $BPL_JMX_ENABLED false enables Java Management Extensions (JMX) [INFO] [creator] $BPL_JMX_PORT 5000 configure the JMX port [INFO] [creator] $BPL_JVM_HEAD_ROOM 0 the headroom in memory calculation [INFO] [creator] $BPL_JVM_LOADED_CLASS_COUNT 35% of classes the number of loaded classes in memory calculation [INFO] [creator] $BPL_JVM_THREAD_COUNT 250 the number of threads in memory calculation [INFO] [creator] $JAVA_TOOL_OPTIONS the JVM launch flags The Spring Boot buildpack creates a layered image, which is beneficial if you want to introduce updates to your application because in this case, only the required layers will be updated, and the others are pulled from the cache: [INFO] [creator] Paketo Buildpack for Spring Boot 5.31.2 [INFO] [creator] https://github.com/paketo-buildpacks/spring-boot ... [INFO] [creator] Creating slices from layers index [INFO] [creator] dependencies (18.9 MB) [INFO] [creator] spring-boot-loader (457.2 KB) [INFO] [creator] snapshot-dependencies (0.0 B) [INFO] [creator] application (41.0 KB) Finally, you can see the layers added to the final image: Image layers [INFO] [creator] ===> EXPORTING [INFO] [creator] Adding layer 'paketo-buildpacks/ca-certificates:helper' [INFO] [creator] Adding layer 'paketo-buildpacks/bellsoft-liberica:helper' [INFO] [creator] Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties' [INFO] [creator] Adding layer 'paketo-buildpacks/bellsoft-liberica:jre' [INFO] [creator] Adding layer 'paketo-buildpacks/executable-jar:classpath' [INFO] [creator] Adding layer 'paketo-buildpacks/spring-boot:helper' [INFO] [creator] Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings' [INFO] [creator] Adding layer 'paketo-buildpacks/spring-boot:web-application-type' [INFO] [creator] Adding layer 'buildpacksio/lifecycle:launch.sbom' [INFO] [creator] Added 5/5 app layer(s) [INFO] [creator] Adding layer 'buildpacksio/lifecycle:launcher' [INFO] [creator] Adding layer 'buildpacksio/lifecycle:config' [INFO] [creator] Adding layer 'buildpacksio/lifecycle:process-types' Let’s check the image: docker images REPOSITORY TAG IMAGE ID CREATED SIZE buildpacks-demo 0.0.1-SNAPSHOT 159ae953af03 1 minute ago 346MB Isn’t that amazing? We didn’t write a Dockerfile or use third-party tools: just one simple command, and you get a ready container image with your application! If you run docker history, you will get the information about image layers and their sizes: docker history output docker history buildpacks-demo:0.0.1-SNAPSHOT IMAGE CREATED CREATED BY SIZE COMMENT 34262f088371 N/A Buildpacks Process Types 69B N/A Buildpacks Launcher Config 1.95kB N/A Buildpacks Application Launcher 2.56MB N/A Application Slice: 5 0B N/A Application Slice: 4 5.33kB N/A Application Slice: 3 0B N/A Application Slice: 2 399kB N/A Application Slice: 1 19.8MB N/A Software Bill-of-Materials 311kB N/A Layer: 'web-application-type', Created by bu… 3B N/A Layer: 'spring-cloud-bindings', Created by b… 76.9kB N/A Layer: 'helper', Created by buildpack: paket… 2.9MB N/A Layer: 'classpath', Created by buildpack: pa… 11B N/A Layer: 'jre', Created by buildpack: paketo-b… 207MB N/A Layer: 'java-security-properties', Created b… 214B N/A Layer: 'helper', Created by buildpack: paket… 4.5MB N/A Layer: 'helper', Created by buildpack: paket… 3.79MB N/A 505B N/A 1.42kB N/A 26MB N/A 77.9MB As you can see, the top layer is extremely small, which means that the updates in Kubernetes will be fast. Run the Docker image Let’s run our containerized application to verify that it is functioning correctly. When we run our application in a container, the JVM flags are automatically set based on the container image size and application properties that we specified. Upon stopping the application, you can get statistics on memory usage, which will enable you to adjust the JVM setting. Back to our app! As our image is published to the Docker registry, we can access it with the following command. The -m flag sets the memory limit for the container; in the case of this app, 1 GB should be enough: docker run --rm -p 8080:8080 -m 1g docker.io/library/buildpacks-demo:0.0.1-SNAPSHOT ... 2024-09-26T08:08:55.838Z INFO 1 --- [buildpacks-demo] [ main] c.e.b.BuildpacksDemoApplication : Started BuildpacksDemoApplication in 3.242 seconds (process running for 4.2) If you visit localhost:8080, you should see our message “Hello! We love Paketo buildpacks!” Note the time it took the application to start: I will show you how to optimize it by leveraging Spring Boot support for CDS and AOT. After you stop the container, you will see a similar output in your console: Memory tracking statistics Native Memory Tracking: Total: reserved=910041934, committed=103297870 malloc: 21488462 #128396 mmap: reserved=888553472, committed=81809408 - Java Heap (reserved=469762048, committed=26886144) (mmap: reserved=469762048, committed=26886144, at peak) - Class (reserved=67897516, committed=4720812) (classes #7152) ( instance classes #6641, array classes #511) (malloc=788652 #20245) (peak=789684 #20249) (mmap: reserved=67108864, committed=3932160, at peak) ( Metadata: ) ( reserved=67108864, committed=25559040) ( used=25208784) ( waste=350256 =1.37%) ( Class space:) ( reserved=67108864, committed=3932160) ( used=3657536) ( waste=274624 =6.98%) - Thread (reserved=12624976, committed=918608) (threads #12) (stack: reserved=12582912, committed=876544, peak=876544) (malloc=28432 #83) (peak=74352 #189) (arena=13632 #24) (peak=307416 #24) - Code (reserved=254561352, committed=13020232) (malloc=928840 #4931) (at peak) (mmap: reserved=253632512, committed=12091392, at peak) (arena=0 #0) (peak=34696 #2) - GC (reserved=1559722, committed=122026) (malloc=23722 #80) (peak=108362 #103) (mmap: reserved=1536000, committed=98304, at peak) - Compiler (reserved=5668968, committed=5668968) (malloc=29392 #309) (peak=68960 #187) (arena=5639576 #10) (peak=45249272 #29) - Internal (reserved=494638, committed=494638) (malloc=457774 #11402) (peak=473623 #11333) (mmap: reserved=36864, committed=36864, at peak) - Other (reserved=0, committed=0) (malloc=0) (peak=22528 #3) - Symbol (reserved=10486636, committed=10486636) (malloc=9201612 #81641) (at peak) (arena=1285024 #1) (at peak) - Native Memory Tracking (reserved=2086944, committed=2086944) (malloc=32608 #568) (peak=34776 #597) (tracking overhead=2054336) - Shared class space (reserved=16777216, committed=12320768, readonly=0) (mmap: reserved=16777216, committed=12320768, peak=12558336) - Arena Chunk (reserved=24624, committed=24624) (malloc=24624 #263) (peak=46177496 #1046) - Tracing (reserved=257, committed=257) (malloc=257 #5) (at peak) - Statistics (reserved=128, committed=128) (malloc=128 #2) (at peak) - Arguments (reserved=165, committed=165) (malloc=165 #5) (at peak) - Module (reserved=65592, committed=65592) (malloc=65592 #1654) (at peak) - Safepoint (reserved=8192, committed=8192) (mmap: reserved=8192, committed=8192, at peak) - Synchronization (reserved=736600, committed=736600) (malloc=736600 #7072) (peak=736808 #7074) - Serviceability (reserved=17152, committed=17152) (malloc=17152 #9) (peak=17296 #11) - Metaspace (reserved=67267488, committed=25717664) (malloc=158624 #114) (at peak) (mmap: reserved=67108864, committed=25559040, at peak) - String Deduplication (reserved=680, committed=680) (malloc=680 #8) (at peak) - Object Monitors (reserved=1040, committed=1040) (malloc=1040 #5) (peak=5824 #28) - Unknown (reserved=0, committed=0) (mmap: reserved=0, committed=0, peak=20480) Based on this data, you can further tune JVM memory settings as I mentioned above. Create a native image using buildpacks Spring Boot buildpacks also enable you to turn your application into a containerized native executable using the GraalVM Native Image compiler. You can read more about using Native Images with Spring Boot here, but in short, the key benefits of the technology is that native images start faster and at peak performance. In addition, there’s no memory overhead associated with the JVM warmup, which helps to avoid allocating more memory to your instances than needed. To create a containerized native image with buildpacks, you need to add a GraalVM plugin to your configuration file. Maven: org.graalvm.buildtools native-maven-plugin Gradle: plugins { java id("org.graalvm.buildtools.native") version "0.10.3" } After that, run the following command. Maven: mvn spring-boot:build-image -Pnative Gradle: gradle bootBuildImage Note that building a native image is a resource-demanding process that requires several gigabytes of memory. In addition, the build usually takes several minutes, but trust me, it’s worth the wait! Under the hood, Spring Boot buildpacks use Liberica Native Image Kit, a GraalVM CE-based native-image compiler, and Ubuntu Jammy tiny that has a smaller memory footprint. Check the images: docker images REPOSITORY TAG IMAGE ID CREATED SIZE buildpacks-demo 0.0.1-SNAPSHOT 5929c51935eb 44 years ago 111MB The resulting image is 3 times smaller than the standard one! This is because the native executable doesn’t contain the full library of JVM classes. But note that this will not always be the case, everything depends on the complexity of your project. Let’s see how fast it starts. Run the image with: docker run --rm -p 8080:8080 docker.io/library/buildpacks-demo:0.0.1-SNAPSHOT ... 2024-09-26T09:07:39.935Z INFO 1 --- [buildpacks-demo] [ main] c.e.b.BuildpacksDemoApplication : Started BuildpacksDemoApplication in 0.055 seconds (process running for 0.064) As you can see, the application started in just 0.055 seconds, almost instantly! Customize a buildpack Buildpacks are not black boxes that you can’t control: it is possible to tune the build process to get the exact container image you are looking for. Below are some common optimizations you can perform. Choose a Java version In the previous sections, I used JDK 21, but you can specify another LTS release if you like: 25, 17, 11, or even 8, plus non-LTS releases. For this purpose, add the following configuration to the Maven plugin: org.springframework.boot spring-boot-maven-plugin 17 Or to the Gradle plugin: tasks.named("bootBuildImage") { environment.putAll(mapOf( "BP_JVM_VERSION" to "17" )) } Choose another builder You can choose from a variety of builders provided by Paketo or other vendor addhering to Paketo specifications. For instance, you can use Alpaquita Buildpacks based on Liberica JDK Lite optimized for the cloud and lightweiht Alpaquita Linux that comes with two libc variants, optimized musl and glibc. Using this buildpack will help you to reduce the final image size. To use the Alpaquita buildpack, you need to specify another builder in your configuration file. Maven: org.springframework.boot spring-boot-maven-plugin bellsoft/buildpacks.builder:musl Gradle: bootBuildImage { builder = "bellsoft/buildpacks.builder:musl" } After that, use the standard command for containerizing the app with a buildpack and check the images when the build is ready: docker images REPOSITORY TAG IMAGE ID CREATED SIZE buildpacks-demo 0.0.1-SNAPSHOT 4d8acb81e41f 44 years ago 140MB The resulting image is 60% smaller than the first image we have built! Configure the JVM You can use the JAVA_TOOL_OPTIONS environment variable to set JVM-specific options such as the Garbage Collector implementation, heap size, etc. For instance, let’s specify another GC in the Maven plugin: org.springframework.boot spring-boot-maven-plugin -XX:+UseZGC Or for Gradle: tasks.named("bootBuildImage") { environment.putAll(mapOf( "JAVA_TOOL_OPTIONS" to "-XX:+UseZGC" )) } You can also set BP_JVM_JLINK_ENABLED to true to cut out a custom JRE. The jlink tool will generate a custom JRE with the following default options: --no-man-pages, --no-header-files, --strip-debug, --compress=1. To change the options, use the BP_JVM_JLINK_ARGS environment variable. Enabling jlink could be useful if you want to reduce the image size even further. Enable CDS and AOT for faster startup If you are not ready to implement GraalVM Native Image in your project, but reducing application startup is vital, you can use CDS. The startup reduction will not be as drastic as in the case of native images, but in turn, you don’t have to do much to use it with your project. You can enable CDS and AOT processing, which doesn’t impose as strict restrictions as Native Image, but shifts some work to build time. Enable CDS and AOT and add process-aot goal to the Maven plugin: org.springframework.boot spring-boot-maven-plugin true true process-aot Or, if you use Gradle: plugins { java id("org.graalvm.buildtools.native") version "0.10.3" } tasks.named("bootBuildImage") { environment.putAll(mapOf( "BP_SPRING_AOT_ENABLED" to "true", "BP_JVM_CDS_ENABLED" to "true" )) } After that, you can build the buildpack with the usual command: mvn spring-boot:build-image for Maven or gradle bootBuildImage for Gradle. Check the images: docker images REPOSITORY TAG IMAGE ID CREATED SIZE buildpacks-demo 0.0.1-SNAPSHOT 9190499f73a0 44 years ago 396MB Start the container image: docker run --rm -p 8080:8080 docker.io/library/buildpacks-demo:0.0.1-SNAPSHOT ... 2024-09-26T11:18:13.650Z INFO 1 --- [buildpacks-demo] [ main] c.e.b.BuildpacksDemoApplication : Started BuildpacksDemoApplication in 1.6 seconds (process running for 2.395) As you can see, the containerized application with CDS and AOT enabled starts two times faster than the regular container image. Conclusion To sum up, buildpacks facilitate deployment and eliminate the need to maintain and update Dockerfiles. In case the default settings are not enough, you can easily customize the buildpack to meet your requirements. - [Liberica JDK 8u432, 11.0.25, 17.0.13, 21.0.5, and 23.0.1 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u432-11-0-25-17-0-13-21-0-5-and-23-0-1-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u441, 7u441, 8u431, 11.0.24.0.1, 17.0.12.0.1, 21.0.4.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u432, 11.0.25, 17.0.13, 21.0.5, 23.0.1 with non-critical fixes and general improvements. The release contains 1169 fixes and backports overall. BellSoft participated in eliminating 18 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 6 security issues (CVEs) fixed. 73 total security fixes (+ 28 additional non-security fixes) in CPU release: in Liberica 6u441: 13 security fixes + 8 additional fixes; in Liberica 7u441: 13 security fixes + 5 additional fixes; in Liberica 8u431: 13 security fixes + 1 additional fix; in Liberica 11.0.24.0.1: 13 security fixes + 7 additional fixes; in Liberica 17.0.12.0.1: 12 security fixes + 5 additional fixes; in Liberica 21.0.4.0.1: 9 security fixes + 2 additional fixes. In addition, PSU releases include a total of 1068 bugs and backports fixed: in Liberica 8u432: 13 security fixes (+ 2 in FX) + 58 additional fixes (+ 14 in FX); in Liberica 11.0.25: 13 security fixes (+ 2 in FX) + 163 additional fixes (+ 18 in FX); in Liberica 17.0.13: 12 security fixes (+ 2 in FX) + 334 additional fixes (+ 12 in FX); in Liberica 21.0.5: 9 security fixes (+ 2 in FX) + 353 additional fixes (+ 10 in FX). in Liberica 23.0.1: 8 security fixes (+ 2 in FX) + 23 additional fixes (+ 18 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-42950 7.5 javafx web network high none required unchanged high high high CVE-2024-25062 7.5 javafx web network low none none unchanged none none high CVE-2024-21235 4.8 hotspot compiler network high none none unchanged low low none CVE-2024-21208 3.7 core-libs java.net network high none none unchanged none none low CVE-2024-21210 3.7 hotspot compiler network high none none unchanged none low none CVE-2024-21217 3.7 core-libs serialization network high none none unchanged none none low Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 23 CVE-2023-42950 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2024-25062 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2024-21235 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2024-21208 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2024-21210 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2024-21217 𑇐 𑇐 𑇐 𑇐 𑇐 Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [A Guide to Java Stream API](https://bell-sw.com/blog/a-guide-to-java-stream-api/): Processing data sequences without numerous for-loops in Java is possible with Stream API that allows for handling data using functional-style operations. This tutorial will guide you through key concepts of Java Stream API, how to create streams and process data using various operations, and how to use Stream Gatherers, a powerful addition to Stream API in JDK 22 for creating custom operations. Table of Contents Introduction to Java streams Core concepts of Java streams Creating streams Finite streams Infinite streams Parallel streams Intermediate operations with streams filter() map() flatMap() sorted() distinct() skip() limit() Custom intermediate operations with Stream Gatherers Terminal operations with streams collect() forEach() reduce() count() / average() / min() / max() findFirst() / findAny() allMatch() / anyMatch() / noneMatch() Dealing with null in streams Conclusion Introduction to Java streams Stream API was introduced in Java 8 to equip developers with a way of processing collections of elements using the functional programming approach. If we wanted to define functional programming in broad strokes, we would say that functional programming, being the type of declarative programming, describes the desired results we want the program to achieve rather than a sequence of steps the program needs to go through to get these results. The latter is the domain of imperative programming. So, in functional programming, we describe or use a ready function that we want to apply to an element. We can also give hints to the API how to control element selection from the data source, but that’s it: the method of looping over the elements, the arrangement of the source, and other complex tasks like parallelism are left under the hood. To sum up, declarative programming tells the program what needs to be done, and imperative programming — how it needs to be done. Therefore, Stream API enables the creation of streams that consist of elements of a provided collection and functional-style operations that need to be applied to each of these elements without affecting the data source. And lambdas and method references provide a convenient way of using functions with streams. Using Stream API makes code more laconic and prevents modification of the data source. Core concepts of Java streams A stream is a sequence of elements that acts as a data wrapper for a source of elements. A stream allows us to perform multiple operations on the elements of a data source without modifying the original data source. These operations are combined into a stream pipeline. A stream pipeline consists of a source, zero or more Intermediate operations, and a terminal operation: A source can be an array, a collection, an I/O channel, a generator of an infinite sequence of elements, etc. Intermediate operations transform a stream into another stream and support multiple ways of processing data: you can filter, transform the elements, order them, and so on. Terminal operations yield a result or a side effect: for instance, you can count elements, collect them, find a particular element, or perform an action on each element with forEach. A stream pipeline There are two important characteristics of streams to be considered. Streams are lazy, meaning that intermediate operations won’t be executed until a terminal operation is invoked. Also, after executing the terminal operation, the stream closes automatically, and it won’t be possible to reuse it. Creating streams You can create a stream from multiple sources, including collections, arrays, files, functions, static factory methods, etc. In addition, streams can be finite with a predefined number of elements, infinite with a potentially unbound number of elements, and parallel enabling parallel execution on multiple cores. Let’s look at all of that in more detail below. Finite streams We can obtain a stream from a collection with a stream() method: List countries = Arrays.asList("Germany", "France", "Italy"); Stream countriesStream = countries.stream(); countriesStream.forEach(System.out::println); We will discuss the forEach() method below, but for now, it is a terminal operation that enables us to iterate over elements and in this case, print them out. To obtain a stream from an array, we use Arrays.stream(). If you want to get a stream of primitives such as int, long, or double, you can use the dedicated interfaces IntStream, LongStream, or DoubleStream: int[] numbers = {1, 2, 3, 4}; IntStream numbersStream = Arrays.stream(numbers); numbersStream.forEach(System.out::println); Obtaining a stream from a file is possible with Files.lines(): try (Stream fileLinesStream = Files.lines(Paths.get("path/to/file"))) { fileLinesStream.forEach(System.out::println); } catch (IOException e) { throw new RuntimeException(e); } You can also obtain a stream with a static factory method: DoubleStream doubleStream = DoubleStream.of(4.5, 6.7, 1.2); doubleStream.forEach(System.out::println); Infinite streams Infinite streams can be created using a generate() or an iterate() method. In addition, you should set the condition to stop the processing of elements at some point, or else, the program will run indefinitely. It can be done with a limit() method, for instance. With that in mind, let’s create an unbound sequence of random numbers and limit them to 10 elements: Random random = new Random(); IntStream randomIntsStream = IntStream.generate(random::nextInt) .limit(10); randomIntsStream.forEach(System.out::println); In the example above, we created a stream pipeline by unifying the generate() and limit() methods. All operations that you perform on a stream can be merged into a single pipeline, so we can shrink the snippet above: Random random = new Random(); IntStream.generate(random::nextInt) .limit(10) .forEach(System.out::println); Parallel streams You can create parallel streams to divide a bulk of work between several threads. Under the hood,parallel streams use the fork-join framework that splits the task into several subtasks accomplished by worker threads. The results are then merged into a single result. To create parallel streams, you can use a parallel() method on a sequential stream: List nums = Arrays.asList(1, 2, 3, 4, 5); int sum = nums.stream() .parallel() .map(i -> i + 1) .reduce(0, Integer::sum); Or a parallelStream() method: List nums = Arrays.asList(1, 2, 3, 4, 5); int sum = nums.parallelStream() .map(i -> i + 1) //this method adds 1 to each element .reduce(0, Integer::sum); //this method sums all elements Note that using parallel streams is not always beneficial for performance. You can find a good comparison of parallel vs sequential streams in this article. But as a rule, the more elements you have to process and the less computation applied to each element, the bigger the performance gains with parallel streams. Intermediate operations with streams You have already seen some intermediate operations in action in sections above, let’s look at them and some other common operations in more detail. filter() The filter() method helps us to filter the elements by a specific parameter represented by a boolean condition. Only the elements that match the requirement will be sent down the pipeline for further processing. For instance, let’s find and print out all even numbers: List numbers = Arrays.asList(4, 7, 3, 8, 1, 2); numbers.stream() .filter(n -> n % 2 == 0) .forEach(System.out::println); map() The map() method accepts a stream of elements, performs some operation on each element, and returns a stream of modified elements. Schematically, it looks like that: Where T is the type of incoming elements, and A is the type of outcoming elements. But this operation doesn’t change the elements of the source, guaranteeing the immutability of the original. For instance, let’s convert all String elements to lowercase: List words = Arrays.asList("Apple", "Banana", "Pear"); words.stream() .map(String::toLowerCase) .forEach(System.out::println); flatMap() The flatMap() method enables us to obtain a stream for each element of the incoming stream, and then unify these streams into one. For instance, we have two classes, Order and Product: static class Order { List products; double totalPrice; public Order(List products) { this.products = products; calculatetotalPrice(products); } private void calculatetotalPrice(List products) { for(Product product : products) { totalPrice += product.price(); } } public List getProducts() { return products; } public double getTotalPrice() { return totalPrice; } } record Product (String name, int price) { } Note the calculatetotalPrice() method: I will show you how to use a stream instead of a for-loop later on. Having a List of orders, we can obtain a stream of products for each order and then combine them into one stream of products and perform some action on each product (get a name, for instance): Product chair = new Product("Chair", 150); Product table = new Product("Table", 350); Product pencil = new Product("Pencil", 2); Order firstOrder = new Order(Arrays.asList(chair, table)); Order secondOrder = new Order(Arrays.asList(pencil, chair)); List orders = Arrays.asList(firstOrder, secondOrder); orders.stream() .flatMap(order -> order.getProducts() .stream()) .map(Product::name) .forEach(System.out::println); sorted() The sorted() method enables us to sort the elements of the stream. Let’s take a look at the example above: we have a stream of Strings, and we can simply add sorted() without parameters to sort them in natural (here, alphabetical) order, or we can use Comparator.reverseOrder() in this method to sort the elements in descending order: orders.stream() .flatMap(order -> order.getProducts() .stream()) .map(Product::name) .sorted(Comparator.reverseOrder()) .forEach(System.out::println); distinct() Let’s continue with orders and products. Both orders include the same product Chair, so the output of the examples above will contain a repeating String. To get rid of duplicate elements, we can use the distinct() method: orders.stream() .flatMap(order -> order.getProducts() .stream()) .map(Product::name) .distinct() .sorted(Comparator.reverseOrder()) .forEach(System.out::println); skip() The skip() method enables us to skip first N elements of the stream: List numbers = Arrays.asList(4, 7, 3, 8, 1, 2); numbers.stream() .skip(2) .forEach(System.out::println); limit() We have already seen the limit() method in action above, when we created an infinite stream. But you can, of course, use it with finite streams as well to limit the number of elements: List numbers = Arrays.asList(4, 7, 3, 8, 1, 2); numbers.stream() .limit(4) .forEach(System.out::println); Custom intermediate operations with Stream Gatherers Stream API provides a limited albeit rich set of intermediate operations. Sometimes, you need to apply custom logic not supported by Java streams. Luckily, Java 22 introduced a solution to that issue: Stream Gatherers. A new intermediate operation Stream::gather(Gatherer) allows us to process elements of a stream by applying user-defined logic in a gatherer, which is an instance of a Gatherer interface. A gatherer is defined by four functions, some of which are optional: Initializer (optional) provides an object with a private state, which is useful when a gatherer needs to compare new elements to the previous ones; Integrator (obligatory) integrates a new element from an input stream. It can also inspect the private state object and emit elements to the output stream; Combiner (optional) can evaluate elements in parallel if you use parallel streams; Finisher (optional) is invoked when there are no more input elements to consume. You can create your own gatherers by implementing the Gatherer interface or take advantage of built-in gatherers from the Gatherers class. Let’s see how we can implement gatherers in practice. Note that Stream Gatherers are still in preview, so you need to enable preview language features with a command line --enable-preview flag or select Java 23 Preview features in IntelliJ IDEA via Project Structure → Project → Language Level. First, let’s implement a custom gatherer that enables us to collect unique users by country. Create a simple record User: record User (String username, String country) { } Now, we need to create a DistinctByCountryGatherer class that implements the Gatherer interface: public class DistinctByCountryGatherer implements Gatherer, User> { } The Gatherer interface has three parameters, where: T is a type of an input element. In our case, it will be an object of class User. A is a type of a gatherer’s private state object that can be used to track the previously seen elements. In our case, it’s a Set representing a country. R is a type of an output element. In our case, it is the same as the input parameter. The DistinctByCountryGatherer has two type parameters, User and String. Next, we need a function that will be applied to the elements of a stream: private final Function selector; public DistinctByCountryGatherer(Function selector) { this.selector = selector; } We also need to override and update two methods, initializer() and integrator(): @Override public Supplier> initializer() { return HashSet::new; } @Override public Integrator, User, User> integrator() { return Integrator.ofGreedy((set, user, downstream) -> { String extractedCountry = selector.apply(user); if(!set.contains(extractedCountry)) { set.add(extractedCountry); downstream.push(user); } return true; }); } As mentioned above, the initializer() function provides a private state object for the gatherer ro compare elements. We will store country names in a Set to identify unique ones. The integrator() function is created by calling the Integrator.ofGreedy() method, which takes a lambda expression as an argument with three parameters: set is is our private state object, user is the next input element, downstream is an object of Gatherer.Downstream that pushes its argument down the stream pipeline. We extract the country name from the User object by applying the selector function to it. If our private state object HashSet doesn’t contain this country name, it is added to the stream, and the User object is pushed down the pipeline. Otherwise, the object is discarded. Let’s use our custom Gatherer in a pipeline: List users = Arrays.asList( new User("francesca", "Italy"), new User("mark", "Germany"), new User("paolo", "Italy")); users.stream() .gather(new DistinctByCountryGatherer<>(User::country)) .forEach(System.out::println); The output will be: User[username=francesca, country=Italy] User[username=mark, country=Germany] You can combine multiple custom gatherers in one stream pipeline, creating tailor-made element processing. Terminal operations with streams We had to use some terminal operation above such as forEach() and reduce() simply because our intermediate wouldn’t execute without them. But it’s time to familiarize ourselves with more terminal operations! collect() The collect() method enables us to accumulate the elements of the stream into a collection or a mutable result container (for instance, you can concatenate all Strings into one String). Let’s take the example with orders and instead of printing out the results, collect them into a Set: Set products = orders.stream() .flatMap(order -> order.getProducts() .stream()) .map(Product::name) .collect(Collectors.toSet()); In the case you want to gather the results into a List, you can simplify the code and use toList() instead of collect(Collectors.toList()): List products = orders.stream() .flatMap(order -> order.getProducts() .stream()) .map(Product::name) .distinct() .sorted(Comparator.reverseOrder()) .toList(); forEach() The forEach() method iterates over the elements and performs a specified action on each. We used it to print out the elements of the stream, which is the most common operation with this method. reduce() The reduce() method is the reduction operation meaning that it produces a single result for a stream of elements. There are three concepts associated with reduce() we need to understand: Identity is the initial value of the operation and the default value in case the stream is empty; Accumulation function takes a partial result of the operation and the next element of a stream; Combining function combines the partial result in case of a parallelized reduction operation. So, these are signatures for the reduce() method: reduce(BinaryOperator accumulator) accepts the accumulation function and returns an Optional; reduce(T identity, BinaryOperator accumulator) accepts the provided identity and an accumulation function and return a single value; reduce(U identity, BiFunction accumulator, BinaryOperator combiner) accepts the provided identity, an accumulation and combining functions and returns a U. Let’s look at the example. Previously, we used a for-loop to calculate a total price for an order. Instead of iterating over each element manually, we can create a stream of Integers (product prices) and obtain the sum of all elements: private void calculatetotalPrice(List products) { totalPrice = products.stream() .map(pr -> pr.price) .reduce(0, (x, y) -> x + y); } Here, value 0 is the identity that serves as the initial value of the operation, the lambda expression (x, y) -> x + y is the accumulation function, where x is the partial result and y is the next element of the stream. You can simplify the code above by using the method reference: private void calculatetotalPrice(List products) { totalPrice = products.stream() .map(pr -> pr.price) .reduce(0, Integer::sum); } count() / average() / min() / max() You can count the number of elements in the stream with count(): List nums = Arrays.asList(1, 2, 3, 4, 5); long numberOfElements = nums.stream().count(); Find a min or max value among elements using min() and max() that accept a Comparator. Both methods return an Optional because a result may not exist. As a result, you can receive an Optional or throw an exception: List nums = Arrays.asList(1, 2, 3, 4, 5); int minNumber = nums.stream() .min(Comparator.comparing(Integer::intValue)) .orElseThrow(NoSuchElementException::new); Optional maxNumber = nums.stream() .max(Comparator.comparing(Integer::intValue)); You can also count the average of elements with the average() method: int[] numbers = {1, 2, 3, 4, 5, 6}; OptionalDouble average = Arrays.stream(numbers) .average(); findFirst() / findAny() With the findFirst() method, you can get the first element of the stream. The method returns an Optional: String firstProduct = orders.stream() .flatMap(order -> order.getProducts() .stream()) .map(Product::name) .findFirst() .orElse(null); The findAny() method returns any element of the stream. Be careful with this one: the method returns a random element regardless of its position in the stream, and there’s no guarantee that the same element will be chosen every time you invoke this method. String anyProduct = orders.stream() .flatMap(order -> order.getProducts() .stream()) .map(Product::name) .findAny() .orElse(null); allMatch() / anyMatch() / noneMatch() The allMatch(), anyMatch(), and noneMatch() methods help you determine whether the elements meet the provided condition and return a boolean. The allMatch() method determines whether all elements satisfy the condition, anyMatch() — whether any one element satisfies the condition, and noneMatch() — whether no elements satisfy the condition. In all cases, the element processing stops as soon as the answer is determined: boolean allOrdersMatch = orders.stream() .allMatch(order -> order.getProducts().size() > 1); boolean anyOrderMatches = orders.stream() .anyMatch(order -> order.gettotalPrice() > 300); boolean noOrderMatches = orders.stream() .noneMatch(order -> order.gettotalPrice() > 1000); Dealing with null in streams When working with streams, we can come across a NullPointerException if we try to perform actions on a Collection with a null value or on elements of a stream with a null value. Luckily, we can make our code NPE-proof in both situations. To prevent processing a null stream, we can take advantage of the Optional.ofNullable() method, which creates an Optional of a provided collection. In case the collection is null, it creates an empty stream: List nullList = null; Optional.ofNullable(nullList) .stream() .forEach(System.out::println); What if some objects in a stream are null? In this case, we can simply filter them out with filter(): List listWithNulls = Arrays.asList(pencil, null, table); listWithNulls.stream() .filter(Objects::nonNull) .map(Product::name) .forEach(System.out::println); Conclusion To summarize: Stream API enables the developers to process a collection of elements with functional programming tools. Streams describe operations that need to be performed on the elements of a data source without modifying the data source. There are intermediate and terminal operations. Intermediate operations won’t be executed until a terminal operation is invoked. There are multiple default intermediate and terminal operations, but you can also create a custom intermediate operation with Stream Gatherers. Want to know more about cool Java features? Subscribe to our newsletter and get a monthly digest on performance, security, and all things Java. - [Liberica Native Image Kit 23.0.6, 23.1.5, and 24.1.1 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-6-23-1-5-and-24-1-1-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.6 for JDK 17, 23.1.5 for JDK 21, and 24.1.1 for JDK 23 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable improvements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2023-42950 7.5 javafx web network high none required unchanged high high high CVE-2024-25062 7.5 javafx web network low none none unchanged none none high CVE-2024-21235 4.8 hotspot compiler network high none none unchanged low low none CVE-2024-21208 3.7 core-libs java.net network high none none unchanged none none low CVE-2024-21210 3.7 hotspot compiler network high none none unchanged none low none CVE-2024-21217 3.7 core-libs serialization network high none none unchanged none none low Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [BellSoft Releases Container Images with Liberica JDK for Rocky Linux](https://bell-sw.com/blog/bellsoft-releases-container-images-with-liberica-jdk-for-rocky-linux/): We are happy to announce the release of Liberica JDK container images for Rocky Linux! These images are aimed at substituting Liberica JDK images for CentOS. As CentOS Stream 8 reached end-of-live on May 31, 2024, we have decided to provide the developers with an alternative set of OCI container images for Java applications based on an actively developed and community-supported Linux distribution. Rocky Linux is a popular distro designed to be 100% bug-for-bug compatible with Red Hat Enterprise Linux (RHEL). Rocky Linux rebuilds sources directly from RHEL; the builds are enterprise-ready. The release schedule is aligned with that of RHEL, when new major releases come out every May, and minor releases see the light every May and November. Note that with the availability of Liberica JDK images for Rocky Linux, BellSoft’s images for CentOS are considered deprecated. But if you have been using CentOS base images, you can smoothly migrate to Rocky. There are two repositories with JVM images for Rocky Linux: with Liberica JDK and Liberica JRE. Both repositories contain images for LTS JDK versions (8, 11, 17, 21) and the latest non-LTS version (23 currently) for x86_64 and Aarch64 architectures. In addition, there are images with a pre-packaged CDS archive aimed at reducing Java application startup. The image names have the following structure: X-Y, where X is the Java version and Y is the architecture type. Images with a CDS archive are additionally tagged with cds. If no architecture is specified, images for x86_64 are used by default. For instance, to build an application with Liberica JDK 21 for Rocky Linux on x86_64, use the following FROM instruction in your Dockerfile: FROM bellsoft/liberica-openjdk-rocky:21 Browse Liberica JDK images for Rocky Linux on Docker Hub - [Java Developer Survey: Trends and Practices in Java Development](https://bell-sw.com/blog/Java-Developer-Survey-2024/): Executive summary BellSoft recently conducted the Java Developer Survey to explore the key challenges and trends shaping the Java development landscape today. Unlike other Java surveys, this initiative aimed to highlight often-overlooked concerns, focusing on the gap between IT priorities and business needs, understanding real-world usage of new tools and Java features, and identifying emerging industry trends. The survey, conducted by the BellSoft Research Team, gathered insights from over 300 Devoxx Belgium attendees, offering a fresh perspective on what developers encounter in production environments. Part 1. Performance Issues of Java 11 and Older Versions Despite the release of newer Java versions, Java 8 and 11 continue to dominate production environments. According to various sources (New Relic’s 2024 State of the Java Ecosystem or JetBrains’s 2023 Dev Ecosystem Report), 29-50% of respondents continue to use Java 8, while 32-38% rely on Java 11. In our survey, two-thirds of respondents admitted to still running applications on Java 11 or earlier versions, which is quite staggering. We asked respondents why they are not migrating to newer versions of Java and how they address performance issues. Here’s what we discovered: Fact 1: Migrating to Newer Java Versions Is Not a Business Priority When asked why migration is not yet done, 18% cited third-party library dependencies as a primary obstacle. However, the broader trend reveals a disconnect between technical and business priorities. For 21% of respondents, migration is not pursued because businesses do not see it as an urgent need. Another 17% indicated that their teams lack the time and resources for testing and migration, which is also often allocated by the business managers, product and team leads. Business leaders may not see the immediate value in upgrading to new versions of Java, missing potential cost-saving opportunities and leaving companies reliant on older, less efficient and sometimes less secure technology. We were surprised to find that those who do not have applications running on Java 11 or older represent a minority — just over 30%. This finding does not align with the trend we observe in Liberica JDK downloads. Fact 2: Additional Resources Are Spent on Enhancing Performance on Java Versions 11 and Older While some are preparing for future migrations, almost a quarter (23%) of respondents allocate additional budget to improve the performance of applications running on Java 11 and older: 5% have hired external consultants to optimize JVM settings 9% have internal dedicated performance teams working to improve system efficiency Another 9% resort to scaling infrastructure to maintain performance levels Meanwhile, 19% of respondents are taking no action, missing potential opportunities for cost-saving and optimizing resource utilization . Older applications could become more cost-efficient by either reducing resource consumption or handling increased workloads. This could be achieved by migrating to newer Java versions or implementing specialized JDKs like Liberica Performance Edition, which are designed to boost performance without extensive labor costs. Businesses that invest in these strategies stand to gain in both operational efficiency and cost savings. Part 2. Security and Performance At BellSoft, security and performance are the two pillars of our development philosophy. While it's generally assumed that all organizations regularly update their applications and optimize performance, we wanted to verify how these practices are truly applied in production environments. Fact 3: 10% Are Not Updating Their JDK Regularly, Leaving Applications Vulnerable to Security Threats Contrary to expectations, our survey revealed that only 65% of respondents update their applications regularly: 10% update their applications with every JDK release, regardless of whether it's an LTS version 46% rely on LTS releases and ensure regular updates with each CPU/PSU release 9% only apply CPU/PSU updates and migrate to newer Java versions when necessary While keeping software up to date is universally recognized as critical for security, 10% of respondents admitted they only update their JDK when critical issues arise. Additionally, 23% reported that their update policies depend on the specific service being used. Failing to regularly update the JDK poses a significant security risk, leaving applications vulnerable to attacks. While updates can occasionally introduce bugs, applying Critical Patch Updates (CPU) and Patch Set Updates (PSU) is essential for maintaining security and system integrity. CPUs contain security patches and critical bug fixes, while PSUs include all CPU fixes along with additional non-critical fixes and enhancements. Since CPUs include fewer changes, they can be integrated into the project without disrupting the application running in production. For those using outdated versions like Java 6 and 7, it’s crucial to migrate to newer versions (Java 8 or later) or get the extended support from vendors like BellSoft to receive regular updates and maintain security. Those still running Java 5 or earlier have no choice but to migrate, and BellSoft engineers specialize in assisting companies with these kinds of migrations. Fact 4: Delays in Releasing Security Updates and New Versions Are Key Factors in Switching JDK Providers Our survey also found that 25% of respondents would consider switching their OpenJDK provider if there were delays in releasing security updates or new Java versions. The top reason for changing JDK providers, however, was licensing costs and restrictions, cited by 33% of respondents. Other key factors included: Lack of support for necessary platforms or architectures Absence of desired features (e.g., CRaC, GraalVM) Poor performance (19%) and vendor lock-in (16%) Interestingly, fewer than 10% of respondents said that the lack of JRE binaries or inadequate support and lack of service-level agreements (SLA) would lead them to change providers. Additionally, 37% of respondents stated that they are not allowed to choose their stack due to company policy. Fact 5: 73% Believe the Performance of Java Applications in Their Company Can Be Improved Although many assume that production applications are already optimized for performance, only 15% of respondents said that they’re satisfied with the current performance of their Java workloads. A more significant portion (39%) is actively working on improving performance, while a staggering 34% said it is not a priority for their company. This highlights a disconnect between technical teams and operational priorities of business, where performance optimization is often undervalued. Fact 6: 58% Agree That Business Managers Often Undervalue Java’s Potential to Reduce Cloud Costs A majority of respondents (58%) agreed that business managers often overlook Java’s ability to reduce cloud costs, whether through optimization or better resource management. At BellSoft, we see Java as a key driver in reducing cloud expenses. Generally, Java applications are better suited for horizontal scaling, which involves adding more nodes to a cluster to handle increased workloads, as opposed to vertical scaling, where more power (e.g., upgrading CPUs) is added to existing machines. Ideally, resources are calculated based on peak performance data. However, Java applications often experience slow startup and warm-up times, consuming more resources initially than they do later. This makes scaling inefficient and costly, as engineers must allocate more resources than necessary to meet growing demand.This challenge can be addressed in several ways: Client VM from HotSpot, instead of Server VM, can deliver slightly faster startup and improved memory and CPU efficiency. Application Class Data Sharing (AppCDS), coupled with Ahead-of-Time (AOT) processing provided by Spring Boot, can reduce startup times by up to 50%. GraalVM Native Image and Coordinated Restore at Checkpoint (CRaC) can cut startup and warm-up times from seconds to milliseconds. These solutions enable predictable scaling, boost resource efficiency, and enhance response times. They also reduce the risk of system crashes during peak periods, like Black Friday. In other words, optimized Java applications can handle requests faster, more efficiently, and with fewer resources, lowering costs and potentially increasing business margins. Not only can new features drive business margins, but optimizing existing Java workloads can significantly reduce costs and improve operational efficiency. Fact 7: Green Java Is Becoming a Key Focus for 18% of Respondents Green programming in Java refers to the practice of developing software that uses less power, is more sustainable, and has a reduced environmental impact. We’re pleased to see that nearly two-thirds of respondents recognize this as an emerging trend within their companies to varying degrees: 18% stated that Green Java has become an important focus 25% mentioned that it’s a topic of discussion Another 25% indicated growing awareness of Green Java This shift toward environmentally responsible development highlights the increasing role of sustainability in software practices. Fact 8: The Most Popular Java Features in Production Since Java 8, the language has evolved significantly, introducing features that make the development process easier and improve Java’s efficiency. So, which features are most commonly used in production? The leading positions are taken by the language features facilitating the development and maintenance of Java code. They are followed closely by Virtual Threads that enable better resource utilization and at the same time, help to avoid using asynchronous programming models. Records (55%) lead the way as the most popular feature Pattern Matching (53%) follows closely var and Enhanced Switch Expressions tied for third place in popularity Virtual Threads (introduced in JDK 21) have quickly gained traction, becoming a top 5 feature Usage of these features varies: 37% of respondents use only one feature in production 11% use two features 12% use three features 16% use four features 24% utilize five or more features in production environments This diversity in feature adoption shows how Java's evolving toolkit is being integrated into real-world applications, driving both efficiency and innovation. Part 3. AI’s role in Java development Artificial Intelligence (AI) is a hot topic, and we aimed to gauge the adoption rate of AI tools in Java development. Fact 9: 74% of Developers Use AI Tools for Code Writing AI assistance is becoming mainstream, with 74% of developers now leveraging AI tools for writing code. Only 26% of respondents do not use AI for coding, while a small percentage (0.33%) reported that they are not allowed to use AI for code writing in their companies. The most popular AI tools among developers are ChatGPT and GitHub Copilot. These are followed by tools like Idea AI Assistant and OpenAI API. Among those using AI tools: 60% rely on a single AI tool 31% use two AI tools 9% utilize three or more AI tools in their development process Fact 10: Over Half of Those Using AI Frameworks for Engineering Opt for Spring AI Spring AI, launched earlier this year, has quickly become the leading AI framework in the Java ecosystem. Although only 34% of respondents currently use an AI framework, most of them have chosen Spring AI. This tool tackles the core challenge of AI integration: connecting enterprise data and APIs with AI models. Large Language Models (LLMs) and AI tools are new technologies that integrate seamlessly with the existing Java frameworks. This allows developers to enhance development workflows and business logic without stepping outside of the Java platform. In return, the Java platform benefits from the stability and support of the frameworks and JDK distributions, with Liberica JDK, in particular, recommended by Spring. Research Method This year’s report reflects the evolving industry landscape by focusing on three key areas: the performance challenges associated with Java 11 and earlier versions, security and performance concerns, and the growing influence of AI in application development. The survey, distributed at Devoxx Belgium in October 2024, collected 308 completed responses, reflecting a comprehensive view of the current state of Java development. Devoxx is the largest Java community conference series in the world, hosting events across Belgium, France, the UK, Poland, Ukraine, Morocco, and Greece. The Devoxx Belgium 2024 conference anticipated over 3,200 attendees from 45 countries, underscoring its significance in the Java community - [BellSoft adds AArch64 support to Alpaquita Linux distribution and Alpaquita containers](https://bell-sw.com/news/bellsoft-adds-aarch64-support-to-alpaquita-linux-distribution-and-alpaquita-containers/): Arm architecture is gaining traction due to many factors, including the rapid growth of cloud computing and the increasing adoption of ML and AI. Arm-based hardware allows the easy launch and running of containers; meanwhile, Arm servers offer a scalable and cost-effective solution for vast amounts of data and large-scale workloads operations. BellSoft, a leading OpenJDK contributor, adds 64-bit Arm support to its Alpaquita Linux distribution and Alpaquita containers to complete your Java experience in the cloud and on Arm. According to Grand View Research, the global Arm-based server market size is anticipated to grow at a CAGR of 14.3% from 2024 to 2030. With Alpaquita Linux distribution and Alpaquita containers made for Arm, you can combine the latest hardware and modern Java functionalities to yield all the potential of the cloud environment in a low-cost manner. "BellSoft aims to deliver an easy, secure, and performant Java experience. Arm architecture support rounds up our product portfolio, providing all instruments for building sustainable and performant Java applications on Arm with one OpenJDK vendor – BellSoft. We recommend using Liberica JDK Lite and Alpaquita Linux on Arm for Java workloads in the cloud. This combination is especially relevant to organizations with high-density container environments, extensive Java applications, and requirements for extra security, as well as to those looking for cloud optimization. Moving Java workloads from x86_64 to AArch64 enables better performance and lowers costs. "- said Aleksei Voitylov, BellSoft's CTO. Match your Java with the cloud environment via Alpaquita Linux distribution to get the following Java on Arm benefits: Faster application response; Improved startup time; Small static footprint, suitable for containers with Java applications; Full security suite (feature-wise and CVE-wise); Optimal performance & RAM consumption for micro-services applications; Actual end-to-end support for both Linux and Java Runtime with SLA; LTS releases. About BellSoft BellSoft delivers the most complete Java experience with a more secure, reliable, and cost-effective approach to application development on any platform and in any environment. BellSoft is one of the leading contributors to the OpenJDK, and the only vendor that supports current LTS Java versions, legacy JDK 6 & 7 and Liberica NIK. Liberica JDK is the runtime of choice for VMware, Spring Framework, JetBrains, and millions of users worldwide. For more information, visit www.bell-sw.com. - [A guide to Java profiling: tools, techniques, best practices](https://bell-sw.com/blog/a-guide-to-java-profiling-tools-techniques-best-practices/): Java applications can be extremely performant thanks to the inner workings of JVM and dynamic performance optimization. If the SLAs are met and cloud costs are optimal, you don’t have to do anything; premature optimization is the root of all evil, as the saying goes. But there might come the time when the SLAs are broken or cloud expenses have gone through the roof. The application may suffer from memory leaks, eat away at CPU resources, and simply “work too slow.” Identifying the root causes of performance issues is impossible without application profiling. This article will guide you through key concepts and areas of profiling, describe the most popular Java profilers, and outline the best practices for efficient profiling. Table of Contents What is Java profiling? Key areas of Java profiling Top Java profiling tools Free and open source Java profilers Java profilers with freemium licenses Commercial Java profilers IDE plugins for profiling Best practices of Java profiling Conclusion What is Java profiling? Java profiling is the process of analyzing application behavior running on Java Virtual Machine at runtime. It helps gain insights into memory allocation, garbage collection, thread behavior and other JVM operations. Profiling helps to identify performance bottlenecks, gain understanding of how application behaves under load and stress testing, and find code areas requiring optimization. Profiling is an essential procedure for pinpointing the root causes of deteriorated performance of your Java applications. It may also be required if you are adding a critical feature to your application. Profiling usually deals with low-level processes such as CPU and memory usage; therefore, more high-level performance metrics such latency are out of its scope. There are two types of profilers: instrumenting and sampling ones. Instrumenting profilers modify the application’s code by inserting method invocation logging. This approach is easier to implement, for some basic measurements developers don’t even need specific tools. The downside is that instrumenting provides limited information about the code behavior plus introduces serious performance overhead. Sampling profilers periodically (e.g., every 10 ms) receive from JVM the stack trace of each thread running at that point. Then, the profiler aggregates samples and provides a general picture of application execution. This approach is associated with smaller overhead, but it can yield inaccurate results depending on several factors, including how often, for how long, and at which points the profiler took the samples. It is possible to collect samples continuously in the background while the application is running in production. However, in such cases it is crucial to use profiling tools with a minimal overhead. Key areas of Java profiling What are the key areas of Java profiling? Then can be allocated into four main categories: Execution profiling looks into how much time each application method is consuming. You can evaluate CPU time or wall-clock time; Memory profiling examines the memory usage by application objects and garbage collection behavior, identifies memory leaks. One of the most common types of memory profiling is allocations profiling that tracks memory allocation events; Thread profiling is aimed at analyzing thread lifecycle contention, and synchronization, helping to identify thread locking issues; I/O profiling deals with evaluating the speed of read/write operations. Some modern profilers offer additional functionality such as profiling of database queries and web requests. Top Java profiling tools Below is the list of popular Java profilers. The list is by no means exhaustive, there are many other tools offering similar and distinctive functionality, both open source and commercial. Describing each existing profiler would be too much for an article, so I have decided to go with the most common, established profilers plus a couple of novel interesting solutions. Free and open source Java profilers JFR + JDK Mission Control JDK Flight Recorder or Java Flight Recorder (JFR) is a low-overhead profiler built into OpenJDK, which collects data about various JVM events. JFR can record hundreds of event types, plus you can create custom events to suit your needs. JFR recordings are binary log files, which can be analyzed in JDK Mission Control, a set of monitoring and profiling tools with a GUI. JDK Mission Control (JMC) is also free and open-source, but it is not included in the OpenJDK distributions, and has to be downloaded separately from vendors that provide it. For instance, you can get Liberica Mission Control for Windows, macOS x86/ARM, or Linux. You can use JDK Mission Control on its own to analyze JVM processes running locally or remotely: But the true power lies in the combination of JDK Mission Control and JFR. JMC offers multiple reports for analyzing various JVM events: In addition, you can create your own filters to customize built-in reports. Back to JFR. There’s no need to download anything apart from a Java runtime, as OpenJDK distributions already include it. You can enable JFR upon Java application start: java -XX:+UnlockDiagnosticVMOptions \ -XX:+DebugNonSafepoints \ -XX:StartFlightRecording=duration=30s,filename=my-recording.jfr -jar app.jar The -XX:+DebugNonSafepoints flag enables JFR to collect the information outside the safepoints. A safepoint is when the state of the executing thread is well described. Alternatively, you can use the jcmd tool to start and stop the recording: jcmd PID JFR.start jcmd PID JFR.dump filename=my-recording.jfr jcmd PID JFR.stop Finally, you can take advantage of continuous flight recording. As JFR is associated with low overhead, you can let it run silently in the background. In the simplest scenario, simply change the -XX:StartFlightRecording flag arguments to -XX:StartFlightRecording=name=background,maxsize=100m Where maxsize is the limit to the events stored in memory. If you’d like to know more about working with JFR and Mission Control, refer to our series of guides on Java profiling. VisualVM VisualVM is an open-source Java profiler with a graphic interface that uses the JMX API to collect the data. It was first integrated into JDK 6, but later removed from JDK 9. So, you have to download and use it as a standalone tool now. Using VisualVM is pretty straightforward: simply choose a running JVM process on the left and browse the relevant stats in real-time. You can also connect to the remote JVM process. And you can also take an application snapshot and analyze it later. Basic VisualVM functionality includes monitoring CPU and memory usage, garbage collection, running threads, and heap. You can extend this functionality by using ready plugins or even write your own plugin. For instance, there's a startup plugin that enables you to analyze application performance right from the startup. Async Profiler Async Profiler is an open-source low-overhead profiler for OpenJDK runtimes and other HotSpot JVM-based runtimes. Originally, it used only the internal AsyncGetCallTrace API to collect the data about CPU usage, heap allocation, methods, hardware and software performance counters. The AsyncGetCallTrace API enables the profiler to collect the data about frames of a thread outside the safepoint. However, it is an internal ‘unofficial’ API and it only returns information about Java frames. So, a new stack walker that doesn’t depend on the AsyncGetCallTrace API was implemented. It can be enabled with --cstack=vm and is supported on Linux x64 and Linux Aarch64 with HotSpot JVM. The Async Profiler builds are available for Linux and macOS. The profiler is really small, and there’s no GUI, so you can use it as a standalone CLI tool or embed it into another solution. Using it is simple. You can profile a running application: asprof -e cpu -d 30 -f profile.html PID Or attach the profiler as a Java agent upon application startup: java -agentpath:/path/to/libasyncProfiler.so=start,event=cpu,file=profile.html -jar app.jar The profiler can produce flame graphs and JFR files. Java profilers with freemium licenses New Relic New Relic is not only a profiler, but an observability platform that offers a wide variety of features. Profiling is only part of the pack that also includes anomalies alerts, infrastructure monitoring, security testing, and more. The available services depend on the Subscription Plan. The solution provides multiple dashboards that visualize your data in real-time or post factum. Once you have completed the installation process, you can attach the New Relic agent to the process just like any other profiler: java -javaagent:/full/path/to/newrelic.jar -jar app.jar New Relic offers paid plans for enterprises and a free plan for one basic user and 100 GB of data ingest. Digma.ai Digma differs from the profilers described above in a way that it continuously observes the running code and then analyzes it to produce insights into bottlenecks, code smells, and other performance issues related to the code. It can detect slow DB queries, for instance, or N+1 problems. In addition, it tracks CPU and heap usage, but its main focus is on the code. Digma uses OpenTelemetry to collect the data. It integrates into Intellij IDEA as a plugin and can be used for free if deployed locally or with a paid license if connected to the central environment. Using Digma is easy: simply install the plugin, then follow the instructions to install the Analytics Engine. After that, when you run your code or tests, Digma starts to observe it in the background, producing relevant information about detected issues, which you can analyze without leaving the IDE. Commercial Java profilers JProfiler JProfiler is a robust Java profiler with a UI that offers a comprehensive selection of features including: CPU and memory profiling, a heap walker for identifying memory leaks; Thread profiling; Database calls profiling with support for JDBC and JPA; HTTP calls profiling. In addition, JProfiler can be used for real-time monitoring. The profiler integrates with IDEs as a plugin plus offers facilitated access to JVMs running in Kubernetes or Docker. You have to purchase a license to use JProfiler, but there’s also a free 10-day trial so that you can play around with the tool and see if acquiring a license is worth it. YourKit YourKit offers various profiling features including CPU, memory, GC, database queries, and thread profiling. It can be used to profile local or remote applications and is available as a plugin for several IDEs. YourKit has a UI but can also be used as a command-line tool. The data can be exported into various formats, and support for Open API enables the developers to create custom probes to collect specific data. YourKit requires acquiring a license for development teams, but the vendor also offers free licenses for open-source projects and licenses at lower cost for educational and scientific organization. There’s a 15-day free trial to try out all the profiler's features. IDE plugins for profiling IntelliJ Profiler IntelliJ Profiler uses Async Profiler under the hood and is a part of IntelliJ IDEA Ultimate. As it is an integral part of IDE, there’s no need to set up anything, and one can attach IntelliJ Profiler to the running application with a single click. After that, you can inspect the data produced by the profiler in an output window. The collected data includes total CPU time in the form of a flame graph, method call tree, and JVM events. Best practices of Java profiling Profilers equip the developers with a handful of valuable insights and plenty of data. Not knowing how to treat this data or how to work with profilers renders the whole procedure useless. Below are some key recommendations for smooth and fruitful profiling of your applications: Understand the profiling results. This piece of advice may seem like stating the obvious, but even experienced developers may get lost in the unchartered waters of profiling data. Learn how to analyze profiling data, how to identify performance issues and which parts of the application are to blame. Identify Key Performance Indicators (KPIs). Choose the areas that are most important to optimize plus metrics that will be most optimal for each area. After that, use this data as a reference when studying the profiling data. Don’t over-optimize. Profilers may identify multiple issues, some less critical, and some significantly affecting the application performance. Perfection is impossible, so don’t waste your time on fine-tuning everything. Check against the established KPIs and solve the problems that prevent the team from reaching them. Don’t treat the profiling data as gospel. The profiling data is not always precise; it depends on numerous factors including the length of profiling, frequency of sampling, when the sampling happens, and even the overhead of using the profiler. One way to increase the data reliability is to select the low-overhead profiler, understand its modus operandi, and adjust the period and frequency of sampling. Profile the application under load. Profiling the application in vitro (i.e., on a local machine under minimal load) won’t yield useful results. Performs load testing and stress testing to understand how your application behaves under different loads. In addition, profile the application in real-life to get even more sensible data (see the section below). Consider the production-like environment. Your application is likely to run in a containerized environment, where it may behave differently. Perform profiling in Docker, Kubernetes, or wherever the application is deployed to in production. Conclusion Profiling is an indispensable part of application development. By choosing a right profiler that best suits your needs and setup and following profiling best practices you will be able to acquire valuable data about the behavior of your Java application. And then, you can leverage this information to optimize the application and JVM settings to meet the demands of your users and efficiently use the resources. Subscribe to our newsletter for more articles on Java development and performance optimizations! - [Java 24: What's New in JDK 24?](https://bell-sw.com/blog/an-overview-of-jdk-24-features/): The Holiday Season is three weeks away, but Java developers can already delight in a cool gift from the OpenJDK team, which is a defined set of 24 features in the upcoming JDK 24 release! That’s right, JDK 24 entered a Rampdown Phase One today, and 24 JEPs are targeted for this release, including eight new features! And the best part? Two of these new features start the introduction of the much anticipated OpenJDK Projects into the Java SE Platform: Project Leyden and Project Lilliput! Dive into this article to uncover all 24 new, finalized, enhanced, and removed features in JDK 24! Table of Contents New features 404: Generational Shenandoah (Experimental) 450: Compact Object Headers (Experimental) 475: Late Barrier Expansion for G1 478: Key Derivation Function API (Preview) 483: Ahead-of-Time Class Loading & Linking 493: Linking Run-Time Images without JMODs 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm Finalized features 484: Class-File API 485: Stream Gatherers Enhanced features 487: Scoped Values (Fourth Preview) 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) 489: Vector API (Ninth Incubator) 491: Synchronize Virtual Threads without Pinning 492: Flexible Constructor Bodies (Third Preview) 494: Module Import Declarations (Second Preview) 495: Simple Source Files and Instance Main Methods (Fourth Preview) 499: Structured Concurrency (Fourth Preview) Removed and deprecated features 479: Remove the Windows 32-bit x86 Port 486: Permanently Disable the Security Manager 490: ZGC: Remove the Non-Generational Mode 501: Deprecate the 32-bit x86 Port for Removal Preparation for restrictions and removals 472: Prepare to Restrict the Use of JNI 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe Conclusion New features 404: Generational Shenandoah (Experimental) JEP 404 introduced a generational mode to Shenandoah GC. Shenandoah GC is a low-latency collector that performs most of its work concurrently with the application, including the concurrent compaction, so the pause times will be low regardless of the heap size. But it used to be non-generational meaning that it didn’t divide the objects into young and old generations, like G1 GC, Parallel GC, and as of JDK 23, Z GC. So, developers had to allocate more heap headroom to non-generational Shenandoah. Also, the collector had to mark long-lived objects more frequently and perform more work when collecting the objects. Generational Shenandoah GC will be able to maintain young and old generations to collect young objects more frequently. It will collect either young or both young and old objects, like G1 GC. But its outstanding feature is that it will perform collections concurrently with the application threads. In addition, as it will be able to dynamically adapt its generation sizes and related parameters, it will preserve low pause times, but use less memory, and become more performant generally. Generational mode for Shenandoah GC is currently experimental and has to be explicitly enabled: -XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational The goal is to make Shenandoah GC generational by default in future releases. 450: Compact Object Headers (Experimental) JEP 450 marks the introduction of Project Lilliput to the mainline OpenJDK. The Project is aimed at reducing the size of Java object headers on 64-bit architectures. All objects on the Java heap have headers. An object header can contain a lot of useful information: object pointers, hash code, class, etc. The header consists of a mark word that has multiple purposes (locking, GC data, etc.) and a class pointer that points to the Klass instance. The size of the header is 128 bits on 64-bit platforms regardless of the object size. This feature will merge together the mark word and the class pointer and compact the resulting header to 64 bits. Compacting the size of object headers will reduce the size of the live data, thus potentially reducing the CPU and/or memory footprint of Java programs. This feature will be experimental in JDK 24 and can be enabled with -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders The plan is to enable it by default in future releases. 475: Late Barrier Expansion for G1 JEP 475 introduces late barrier expansion for the G1 Garbage Collector. This feature will help to improve the performance of Java applications in the cloud as it will help to reduce the CPU time and memory overhead during the JVM warmup. To understand the importance of this feature it is necessary to understand what a GC barrier is and how it affects the JVM overhead of JIT compilers. A JIT compiler compiles Java bytecode into machine code using the sea of nodes concept, which is a type of a dependence graph. The compiler first walks the graph from one node to another, expands each node, and performs code transformations, which include optimizations. GC barriers contain information about application memory access. A JIT compiler expands GC barrier nodes just like any other node, at an early compilation stage. But this results in significant compilation overhead. In addition, optimizations can break the barriers, resulting in complex issues. The idea behind late G1 GC barrier extension is to make the JIT compiler expand barrier nodes as late as possible, to the stage when machine code must be emitted. This way, the GC-specific memory access instructions are transformed into machine code according to the relevant barrier information, which was tagged at the early compilation stage. The resulting code may be as performant as C2 optimized code. In addition, late barrier expansion is associated with low C2 overhead. Z GC has been successfully using late barrier expansion since JDK 14. Late barrier extension was one of the factors guaranteeing the stability of this collector. It was decided that G1 GC should be improved in a similar way. So, many techniques developed for Z GC were re-used for G1 GC. 478: Key Derivation Function API (Preview) Classical public-key based cryptographic algorithms are becoming increasingly vulnerable to hacker attacks, especially when it comes to quantum computing, when attacks are performed using a supercomputer. As such, the Java platform needs to provide the developers with means of implementing Post-Quantum Cryptography algorithms to secure their application in a modern, more efficient way. This can be done by integrating support for the Hybrid Public Key Encryption mechanism that uses both public (asymmetric) key encryption and symmetric encryption. The first step towards HPKE in Java was already implemented in Java 21 with the introduction of Key Encapsulation Mechanism API that helps to secure symmetric keys with asymmetric (public key) cryptography. The next step is JEP 478, which introduces a Key Derivation Function API to the Java platform. The key derivation functions will reside in a new javax.crypto.KDF class. Prior to JDK 24, Java didn’t have an API specifically for using KDFs. With this API, the developers will be able to use more sophisticated password-hashing functions, such as Argon2, without dealing with the existing APIs not designed for this purpose. In addition, the support for key derivation functions will be beneficial for applications that interact with cryptographic hardware devices. 483: Ahead-of-Time Class Loading & Linking JEP 483 marks the initial introduction of Project Leyden to the mainline OpenJDK. The startup and warmup times of Java applications can be an issue, especially in the cloud. The startup takes several seconds, and the warmup may take several minutes. The goal of the Project is to reduce the startup and warmup times of Java applications by selectively shifting some computations and optimizations from the production run to some earlier stages, for instance, to the training runs. Ahead-of-Time Class Loading & Linking is the first step in this process. The feature builds on Application Class Data Sharing (CDS). AppCDS reads and parses a set of system and application classes and stores this data in a read-only archive that is then accessible to the JVM at startup. Ahead-of-Time Class Loading & Linking goes further and creates an AOT cache that stores class data after reading, parsing, loading, and linking them. This way, JVM will have even less work to do at startup. Preliminary tests that used a reference Spring Petclinic application showed that AOT cache gives about 40% improvement in startup time. 493: Linking Run-Time Images without JMODs JEP 493 introduces a new feature that will enable the jlink tool to cut the size of the JDK by about 25% by excluding the JMOD files from the run-time JDK images. JMOD files are used by jlink to create a custom Java runtime. With the new option --enable-linkable-runtime enabled at JDK build time, jlink from the resulting JDK will be able to build a custom runtime without the JMOD files. In addition, it is still possible to build a custom runtime with only modules required for the application to run. The absence of JMOD files will benefit such runtimes even more. This feature will not be enabled by default, and it remains at the discretion of JDK vendors to enable it. By the way, Liberica JDK Lite, a flavor of Liberica JDK optimized for the cloud, hasn't included the JMODs files all along. So, you don’t have to wait for JDK 24 and hope that your vendor enables this feature, and instead, use Liberica Runtime Container as an OpenJDK image now! 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism JDK 21 included a new security feature, the Key Encapsulation Mechanism (KEM) API. Building on the momentum of equipping the Java platform with quantum-resistant cryptographic algorithms, JEP 496 introduces an implementation of the quantum-resistant Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM). ML-KEM has been standardized by the United States National Institute of Standards and Technology (NIST) in FIPS 203. The US government computer systems dealing with sensitive information must be upgraded to use ML-KEM. Therefore, the implementation of this mechanism in the Java platform will facilitate the upgrades of Java applications. 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm Continuing on the topic of quantum-resistant security mechanisms in Java, JEP 497 introduces an implementation of the quantum-resistant Module-Lattice-Based Digital Signature Algorithm (ML-DSA). ML-DSA has been standardized by NIST in FIPS 204. This algorithm is used for signing data and authenticating identities in a quantum-resistant fashion. Just like in the case of ML-KEM, the US government computer systems dealing with sensitive information must be upgraded to use ML-DSA. The implementation of this algorithm in the Java platform will facilitate the upgrades of Java applications. Finalized features 484: Class-File API Class files are files containing Java bytecode that can be executed by JVM. They interconnect all parts of the Java ecosystem. Frameworks and other tools such as agents can load or manipulate bytecode to achieve greater performance or gather some data. But they need to understand the bytecode and be able to work with it. There are numerous libraries for generating, parsing, and transforming class files, and frameworks also bundle a class-file library. But the problem is that the class-file format can evolve faster than framework developers update their class-file library, which can lead to parsing errors. JEP 484 finalizes a feature that was introduced in JDK 22 as a Preview, namely, Class-File API. This API will evolve together with the class-file format, so that frameworks and tools won’t have to rely on third-party class-file libraries, but on the standard JDK API, meaning that they automatically use the latest class-file format. It means that developers will be able to update the JDK version for their project without risking in most cases that their current framework or tooling versions will be incompatible with the JDK version. 485: Stream Gatherers JEP 485 finalizes Stream Gatherers, an addition to Stream API that enables the developers to create custom intermediate operations when using Stream API. This feature was introduced as a Preview feature in JDK 22. Steam API offers a wide but limited range of intermediate operations for processing the data. Sometimes, developers need to perform some custom operations with data. For instance, there’s a distinct() intermediate operation that helps to filter unique elements, but there is no distinctBy() operation to filter elements by some other criteria. Steam Gatherers are aimed at solving this issue. By using a new intermediate operation Stream::gather(Gatherer), developers can apply custom logic in a gatherer, which is an instance of a Gatherer interface. It is possible to create your own gatherers by implementing the Gatherer interface or use built-in gatherers from the Gatherers class. Follow this guide on using Stream Gatherers for defining custom data processing operations. Enhanced features 487: Scoped Values (Fourth Preview) JEP 487 introduces a fourth preview of scoped values. Scoped values represent an alternative to thread-local variables and help to write more reliable multi-threaded code. Traditionally, developers used thread-local variables to share data between methods on a call stack without using method parameters. But thread-local variables are mutable, have unbounded lifetime, and can be associated with significant memory footprint. Scoped values have a limited lifetime and share immutable data in one way, from caller to callees. Scoped means that the value is accessible only to certain parts of the program, namely the methods invoked directly or indirectly by the run method. This enables a more reliable and maintainable data sharing process in concurrent code. Scoped values can substitute thread-local variables in many scenarios where one-way transmission of data is implemented. They also bring benefits in terms of memory and time savings when used together with virtual threads and structured concurrency. One major change in this release is the removal of the callWhere and runWhere methods. The only way to use scoped values is via ScopedValue.Carrier.call and ScopedValue.Carrier.run methods. 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) JEP 488 introduces a second preview of primitive types in patterns, instanceof, and switch without changes. Traditionally, patterns, instanceof, and switch haven’t accepted primitive types. It was associated with silent data loss, boilerplate code, and other restrictions. The ability to use primitive types in all pattern contexts, switch expressions and statements, and instanceof will enable uniform data exploration and eliminate the risk of data loss because of unsafe casts. For instance, a following switch statement switch (x.getStatus()) { case 0 -> "okay"; case 1 -> "warning"; case 2 -> "error"; default -> "unknown status: " + x.getStatus(); } Can be improved like this: switch (x.getStatus()) { case 0 -> "okay"; case 1 -> "warning"; case 2 -> "error"; case int i -> "unknown status: " + i; } In addition, as the instanceof type test operator now accepts primitive types, it can perform safeguard casting of primitives: if the left-hand value can be safely converted to the right-hand type without loss of information, instanceof returns true. In cases when no checks are needed regardless of the values (for instance, when we want to cast byte to int), the test always returns true. For other cases, a runtime check is necessary: byte b = 42; b instanceof int; // true int i = 42; i instanceof byte; // true int i = 1000; i instanceof byte; // false 489: Vector API (Ninth Incubator) JEP 489 introduces a ninth incubator of Vector API without changes. This API enables the developers to write vector algorithms that reliably compile at runtime to optimal vector instructions on supported CPU architectures for better performance of scalar operations than with auto-vectorization. Vector API will be incubated until necessary features of Project Valhalla will be integrated as preview. After that, it will be promoted to a preview feature. 491: Synchronize Virtual Threads without Pinning JEP 491 introduces an important enhancement to virtual threads. Virtual threads can mount and unmount platform threads without blocking them, so multiple virtual threads can share one OS thread. However, virtual threads are pinned to the platform threads in synchronized methods, meaning that they cannot unmount the platform thread. It may result in a situation when no more virtual threads can be allocated because all platform threads are taken by the virtual threads or blocked in the JVM, which, in turn, impacts scalability. In JDK 24, the implementation of the synchronized keyword will be changed in such a way that will allow the virtual threads to mount and unmount the platform thread in the synchronized method. This will enable the developers to continue using synchronized methods and at the same time, will eliminate the negative impacts on scalability. In addition, a jdk.VirtualThreadPinned event of the Java Flight Recorder, which was issued in cases when a virtual thread was blocked inside a synchronized method, will be issued in other situations when the virtual thread is pinned, which will help to better diagnose such issues. 492: Flexible Constructor Bodies (Third Preview) JEP 492 introduces a third preview of flexible constructor bodies without significant changes. Flexible constructor bodies was introduced in JDK 22 as JEP 447: Statements before super(...). And that’s what this feature is fundamentally about: it allows the developers to place statements in a constructor before an explicit invocation of another constructor with super(..) or this(..). In this case, it will be possible to avoid writing additional methods and constructors to validate or compute some values necessary for the constructor of the superclass. For example, this code public class PositiveBigInteger extends BigInteger { private static long verifyPositive(long value) { if (value <= 0) throw new IllegalArgumentException(..); return value; } public PositiveBigInteger(long value) { super(verifyPositive(value)); } } Can be expressed in a more laconic way: public class PositiveBigInteger extends BigInteger { public PositiveBigInteger(long value) { if (value <= 0) throw new IllegalArgumentException(..); super(value); } } As the statements cannot reference the object under construction, this feature makes code more concise and maintainable without breaking the rules for safe object initialization. 494: Module Import Declarations (Second Preview) JEP 494 introduces a second preview of module import declarations that enable the developers to implicitly import all package modules by simply importing a module: import module java.base Such an approach simplifies the import of modular libraries, reduces the noise of multiple on-demand package imports, and facilitates coding in Java for beginners. This release brings several changes to the feature: Allow for the import of the whole Java SE API when importing the java.se module; Eliminate ambiguities when several imported modules contain a class with the same name by allowing on-demand import declarations to shadow module import declarations like that: import module java.base; // exports java.util with a public Date class import module java.sql; // exports java.sql with a public Date class import java.sql.Date; // resolve the ambiguity of the name Date Date d = ... // Date is resolved to java.sql.Date 495: Simple Source Files and Instance Main Methods (Fourth Preview) JEP 495 introduces a fourth preview of simple source files and instance main methods with slight changes to the terminology and the title, but without any other changes. This feature enables the developers, who have just started their Java journey, to write simple single-class programs without having to deal with programming-in-the-large, complicated Java concepts that can be encountered even in the simplest possible ‘Hello, World!’ program: public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } This feature allows the main methods to not be static and public and have a String[] parameter. Also, it allows you to skip the class declaration. Finally, some methods for the console input and output are automatically imported. As a result, the program above can be simplified to void main() { println("Hello, World!"); } Simple programs can be evolved with time to ordinary Java programs as the knowledge of the beginning developers grows. 499: Structured Concurrency (Fourth Preview) JEP 499 introduces a fourth preview of structured concurrency without changes. Structured concurrency is aimed at improving the reliability and observability of the concurrent code. If the task is broken into subtasks, and each subtask in its own thread, each subtask can succeed or throw an exception, which may lead to two global problems: If the developers doesn’t explicitly cancel other subtasks in case of errors, it may lead to thread leaks or interference of tasks with each other; Manually handling the lifetime of threads is cumbersome, verbose, and error-prone. Structured concurrency can help to avoid these issues. The feature is based on the principle that if a task is split into subtasks, they all return to the task’s code block. The task monitors subtasks for failures and waits until they finish. Response handle() throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Supplier user = scope.fork(() -> findUser()); Supplier order = scope.fork(() -> fetchOrder()); scope.join() // Join both subtasks .throwIfFailed(); // ... and propagate errors // Here, both subtasks have succeeded, so compose their results return new Response(user.get(), order.get()); } } Entry and exit points of a task’s block are defined clearly, and subtask’s lifetime is confined within the parent code block. This enhances code observability and coordination. If any of the subtasks fail or a thread running the tasks is interrupted before the results of all subtasks are joined, other subtasks get canceled. This increases code reliability. Removed and deprecated features 479: Remove the Windows 32-bit x86 Port The Windows 32-bit x86 Port was deprecated for removal in JDK 21. As the last Windows OS supporting 32-bit operation (Windows 10) will reach end of life in October 2025, there’s no point in supporting this port in OpenJDK. JEP 479 removes the Windows 32-bit x86 Port completely: all code paths applicable to Windows 32-bit x86 only will be removed, and all testing and development for this platform will be stopped. This will help the OpenJDK community to focus on evolutionizing the Java platform by improving and integrating other, more relevant features. 486: Permanently Disable the Security Manager The Security Manager API has been around since the first Java version. It was designed for protecting the applications by means of the principle of least privilege: code is untrusted by default, and developers have to explicitly grant specific code permission to access some resources. In practice, the procedure of giving permissions is so complex that very few applications actually use Security Manager, and it has even been disabled by default. But Java libraries have to implement the principle of least privilege in case the API is enabled, which requires a lot of time and effort. So, it would be better to get read of mainly unused Security Manager than dedicate resources to maintaining compatibility with this API. As a result, the Security Manager API was deprecated in JDK 17, and JVM was made to issue warnings in case Security Manager was enabled. This gave the developers time to shift away from using Security Manager in case they relied on it. And the moment has come: JEP 486 permanently disables the Security Manager API. The API will be removed completely in the future releases. 490: ZGC: Remove the Non-Generational Mode Z GC, a low latency garbage collector with pause times not longer than 10 ms, became generational in Java 21. Maintaining young and old generations enables the GC to collect young objects more frequently, thus reducing the GC overhead and heap memory overhead. Generational mode for Z GC became default in JDK 23, and JEP 490 removes the non-generational mode as it is no longer needed. 501: Deprecate the 32-bit x86 Port for Removal JEP 501 deprecates the 32-bit Linux x86 port, which is the only 32-bit x86 port remaining in the OpenJDK, with the intent of removing it in further releases. As no new hardware is released exclusively for 32-bit x86, and porting new Java features to this architecture requires a lot of work, it will be more beneficial for the OpenJDK contributors to stop maintaining this port and channel their efforts to other, more relevant features and OpenJDK projects. Preparation for restrictions and removals 472: Prepare to Restrict the Use of JNI JEP 472 will make JVM issue warnings on the usage of Java Native Interface (JNI). It will also make JVM issue warnings instead of throwing exceptions when Foreign Function & Memory API (FFM API) is used to align the restrictions for JNI and FFM API. JNI allows Java programs to interact with native code. But these interactions pose risks, including the risk of unexpected JVM crashes, unpredictable GC behavior, and Java code integrity violation. FFM API that was introduced as an alternative to JNI, represents a more reliable approach to interacting with native code, but some risks are applicable to it as well. Restriction of native access is part of an effort of ensuring that the Java platform has integrity by default. The feature lays ground for further restrictions of native access. In the future, the JVM will throw exceptions instead of issuing warnings both for JNI and FFM API. This will make the Java platform more secure and performant. But there’s no goal of prohibiting interaction with native code. Java developers will still be able to load and link native libraries using JNI or FFM API, but they will have to explicitly enable native access at application startup. The goal is to let application developers enable native access, not library developers. 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe The sun.misc.Unsafe class could help to increase the performance of Java applications as it contained methods for low-level operations. However, using these methods without due safety checks could lead to the opposite effect: deteriorated performance and unexpected application behavior. As many libraries used methods of sun.misc.Unsafe without these checks, two more reliable and secure APIs were introduced, Variable Handles and Foreign Function & Memory API. In turn, the memory-access methods in sun.misc.Unsafe were deprecated in JDK 23. JEP 498 will make JVM issue warnings if such methods are used. This is all done with the purpose of preparing the platform for removal of the memory-access methods in sun.misc.Unsafe in future releases. Conclusion Java is getting more powerful with every release, and we are looking forward to the general availability of JDK 24 due in March 2025. Meanwhile, you can subscribe to our blog to get a monthly digest of fresh articles and videos on all things Java! - [Alpaquita Linux Performance on ARM: A Comparative Study](https://bell-sw.com/blog/alpaquita-linux-performance-on-arm-a-comparative-study/): BellSoft recently added support for AArch64 to Alpaquita Linux. Alpaquita Linux is a minimalistic Linux distribution for cloud deployments. It is 100% compatible with Alpine Linux, but comes with several enhancements, including two libc implementations: optimized musl (musl-perf) and glibc. In a previous article, we summarized the results of Linux performance studies on x86. This article provides a summary of performance studies on ARM, where Alpaquita Linux was compared to other popular Linux distributions for cloud using industry-standard benchmarks. Table of Contents Methodology Results Docker image size Startup Performance of glibc vs musl vs musl-perf malloc performance Java benchmarking with DaCapo Memory bandwidth Throughput AsmFish/TSCP Petclinic RAM footprint & latency Conclusion Methodology We ran the tests in a virtual machine on Ampere Altra ARMv8 Neoverse-N1 CPU, which is a server-class machine optimized for handling various cloud-native workloads efficiently. Setup: 4 cores Full virtualization Type 1 hypervisor: KVM Type 2 hypervisor: QEMU A single VM was running on the machine as a workload, so it was dedicated to performance measurement. The following command was used to start the QEMU: qemu-system-aarch64 -cpu host -enable-kvm -hda alpaquita-stream-musl.qcow2 -smp 4 -m 8192 -device virtio-net-pci,netdev=net0 -netdev user,id=net0 -display none -daemonize -machine virt -drive if=pflash,unit=0,readonly=on,file=/usr/share/AAVMF/AAVMF_CODE.ms.fd -drive if=pflash,unit=1,format=raw,file=AAVMF_VARS.ms.fd As Alpaquita Linux comes with two libc implementations, glibc and musl-perf, we tested musl- and glibc-based distributions to compare the performance of two libc implementations. Another musl-based distribution in the tests was Alpine Linux, which comes with stock musl. We also used several popular glibc-based Linux distributions for the cloud: Debian, RHEL, and Oracle Linux. A full list of tested Linux distributions: Alpaquita Linux 23 musl (LTS) Alpaquita Linux 23 glibc (LTS) Alpaquita Linux Stream 23 glibc Alpaquita Linux Stream Slim 23 glibc Alpaquita Linux Stream 23 musl-perf Alpaquita Linux Stream Slim 23 musl-perf Alpaquita Linux with Liberica Native Image Kit, a GraalVM CE-based native-image compiler Alpine Linux 3.20 Debian 11 Slim Debian 12 Debian 12 Slim RHEL 9 UBI Oracle Enterprise Linux 9 Results Docker image size We measured two metrics: base Linux Docker image size and Docker image size with JDK 11. Docker images with and without JDK The base Docker image size of musl-based Alpaquita Linux is 3.38 MB, which is almost 9 times less than that of Debian. The JDK Docker image based on Alpaquita Linux musl is 77 MB, which is 2.7 times smaller than with Debian. Startup The system startup time was measured at different stages: initramfs init: initialization of a root filesystem providing early userspace; mounted root: the root filesystem is mounted; login: the system allows to log in through console; iface is up: the interface is up and running, which allows working with the console; “network”: network services are connected. We also measured application startup using Petclinic, a reference Spring Boot application. Startup results The results of Linux startup studies show that Alpaquita Linux startup till network is 2.5 times faster than that of Debian. As for the application startup, Alpine Linux with JDK demonstrated the worst results. Application startup with Alpaquita Linux with JDK was similar to that with Debian. Petclinic startup with Liberica Native Image Kit took only 0.5 seconds, which is 8 times faster than with JDK. Performance of glibc vs musl vs musl-perf In some situations, the performance of musl libc can be inferior to that of glibc. To solve such issues, we developed optimized musl (musl-perf), which is 100% compatible with stock musl, but has improved performance. The tests in this section were aimed at evaluating the performance of three libc variants: stock musl, musl-perf, and glibc. String operations We ran basic String tests with 1 million iterations and various String lengths: 36 bytes, 123 bytes, and 4,100 bytes because the String size may affect the performance significantly. Results of String operations, 36 bytes Results of String operations, 132 bytes Results of String operations, 4,100 bytes We can see that stock musl demonstrated the worst results in all tests. We can also see that musl-perf has the performance similar to that of glibc. SPEC CPU 2017 The SPEC CPU® 2017 benchmark package includes tests for measuring compute intensive performance. We used two time-measuring suites: SPECspeed®2017 Integer and SPECspeed®2017 Floating Point. The results of both benchmarks are measured in Ratio, which is the run time on the reference platform divided by time on this system. When comparing systems, the system with the higher ratio does more computing per unit of time. Results of SPECspeed®2017 Integer Results of SPECspeed®2017 Floating Point The results of SPEC CPU 2017 tests show that stock musl had inferior performance in most cases compared to glibc and musl-perf. The musl-perf libc demonstrated similar or superior performance to that of glibc in most cases. To sum up, the benchmarking results indicate that enterprises running their workloads on a glibc-based Linux distribution can migrate to Alpaquita Linux with musl-perf without sacrificing performance. malloc performance Linux memory allocators (mallocs) can influence the performance of applications. Alpaquita Linux for AArch64 comes with two additional malloc implementations: mimalloc is a small allocator used in large scalable services with low latency jemalloc enables the developers to solve fragmentation issues and supports scalable concurrency We tested the performance of Alpaquita Linux musl with mimalloc as compared to the default malloc in other distributions. To test the work of mimalloc in various Linux distributions, we used a Mimalloc-bench. The tests measure the number of operations performed in one second. The malloc benchmarks we used for the study are: espresso: a programmable logic array analyzer in the context of cache aware memory allocation alloc-test: simulates intensive allocation workloads with a Pareto size distribution cache-scratch: introduced with the Hoard allocator to test for passive-false sharing of cache lines malloc test results The results of the studies show that Alpaquita Linux musl with mimalloc outperforms other distributions with default mallocs in all tests except for one, cache-scratch-1, where all distributions showed similar results. Java benchmarking with DaCapo DaCapo is a set of real-world Java applications used for measuring system and CPU performance. The results are measured in ms required for the completion of a workload. DaCapo results In all tests, both musl- and glibc-based Alpaquita Linux distributions demonstrated equal or superior performance to that of other distributions. In several tests, including PMD Source Code Analyzer and Avrora AVR Simulation Framework, musl-based Alpaquita demonstrated the best results, which indicated that using musl libc may be more beneficial for some Java workloads than glibc. Memory bandwidth We used the Stream benchmark provided by Phoronix via the Phoronix Test Suite to measure memory bandwidth, i.e., the memory volume we can use at a given time. Stream is a popular RAM benchmark measuring the sustainable main memory bandwidth in MB/s and the computation rate for simple vector kernels. It uses four kernels for different memory operations: Copy: transfer rate measurement without arithmetic operations Scale: adding a simple arithmetic operation Triad: chained/overlapped/fused multiply/add operations Add or Sum: adding a third operand Stream results All distributions demonstrated similar results. Throughput To test the throughput of Linux distributions, we used the Nginx benchmark provided by Phoronix. This benchmark runs on a single host and measures the number of HTTP requests handled per second with a configurable number of concurrent clients. Nginx results Both glibc- and musl-based Alpaquita configurations demonstrated the best results across all tests. Remarkably, glibc-based Alpaquita outperformed other glibc-based distributions, which means that companies that do not want to migrate from glibc to musl can benefit from Alpaquita Linux with glibc. AsmFish/TSCP AsmFish and TSCP provided by Phoronix are chess benchmarks for analyzing CPU performance. TSCP (Tom Kerrigan’s Simple Chess Program) is a small chess engine that calculates how many nodes per second are searched. AsmFish is an advanced chess engine benchmark written in Assembly. The results of both benchmarks are calculated as nodes per second. AsmFish/TSCP results The results of both tests show similar performance of all distributions, with Alpine Linux lagging slightly behind in the AsmFish test, which means that optimized musl outperforms stock musl for these workloads. Petclinic RAM footprint & latency We measured RAM footprint and latency of a Java service based on various Linux distributions at the following conditions: Application: Spring Petclinic Low-end hardware Low load of 133 users and 54 requests per second (RPS) Apart from the combination of JDK and Linux in this series of tests, we also used Alpaquita Linux with Liberica Native Image Kit. RAM consumption results The results of footprint studies show that Oracle Linux 9 demonstrated the highest RAM consumption. Alpaquita Linux with Liberica Native Image Kit demonstrated the lowest RAM consumption, which is 65% better than in case of Oracle Linux. The second and third best results belong to musl- and gilbc-based Alpaquita Linux with JDK respectively. Latency results As for the latency studies, all Linux distributions combined with JDK showed similar results. Liberica Native Image Kit demonstrated 4.7 lower latency at 99 pct as compared to JDK. Conclusion The benchmarking results show that Alpaquita Linux musl demonstrates the best results in terms of Linux startup; In cases where glibc demonstrated superior results to stock musl, Alpaquita Linux with optimized musl has similar or superior results compared to glibc-based distros; Both musl-based and glibc-based Alpaquita distros show better results than other distributions for Java workloads in terms of RAM consumption; Liberica Native Image Kit with Alpaquita shows the best results in terms of a Java application startup time, RAM footprint, and latency. To sum up, Alpaquita Linux provides the best options for enterprises running their Java workloads in the cloud: Optimized musl with superior performance as compared to stock musl in Alpine; A glibc-based version smaller than other popular glibc-based distributions for enterprises not willing to migrate to another libc; Several additional malloc implementations for various workloads; Commercial support with LTS releases; Tools for Java development. Try Alpaquita Linux out with your application and see the difference! Get Alpaquita Linux - [How to Set Up GraalVM Native Image Debugging](https://bell-sw.com/blog/how-to-set-up-graalvm-native-image-debugging/): It is essential to debug and profile native images to understand whether you experience any issues related to the Native Image technology and contact your vendor for advice before pushing the native executable to production. This article offers instructions on creating a native image with debug capabilities in a Docker container and Eclipse IDE. Table of Contents How to debug Native Image Native Image debugging: main considerations Create a native image with debug information in a Docker container Debug native images in Eclipse IDE Conclusion How to debug Native Image Native executables contain the application code, optimized and interpreted into machine code. They contain minimal symbol information, which makes the debugging process challenging. Therefore, to debug native images, we need to generate the debugging information at build time. After that, we provide the debug information to the binary file during the debugging session. Debugging info can be stored on a host or a target machine and sent to the host as needed. This process can be depicted as follows: Native Image Debug Setup It is important to note that a combination of two environments or all three environments can be located on one or multiple machines. For instance, the application that was built in the build environment, in the CI/CD pipeline, for instance, runs in the target environment, e.g., in the cloud. But it can be debugged from the host. There are two ways of setting up a remote debugging session: by using SSH or gdbserver. In both cases, you will have to adjust the setting of your container and network access. Native Image debugging: main considerations There are two factors that you should consider when debugging your native image: Debugging native images is trickier than debugging regular Java applications because there’s no support for Java debugging and Java programs are modeled as C++ programs. So, if you experience any Java-related problems, it is better to debug your application with the usual tools before converting it to native executable. Native Image debugging is available on Linux with limited support on macOS and Windows. Therefore, we recommend debugging a native image in a Docker container or on Linux. If you have macOS or Windows, you can set up a virtual machine with Linux inside. Create a native image with debug information in a Docker container We will use Spring Petclinic as an example application and a Liberica Native Image Kit Container with Liberica Native Image Kit and Alpaquita Linux as a base image to create and run a native image inside a Docker container. First of all, we need to add several build arguments to the GraalVM Maven plugin: The -g flag instructs the native-image tool to generate debug information in the format compatible with the GDB (GNU Debugger). This flag doesn’t affect the execution speed or memory consumption of the native image; The -O0 flag disables compiler optimizations. It is also possible to include additional flags for better debugging experience: The -H:+SourceLevelDebug flag enables including full parameter and local variable information into the debug info. Note that using this flag may result in slower program execution; The -H:-DeleteLocalSymbols flag enables including linkage symbols identifying compiled Java methods into the native image. This info is required for some perf use cases, such as perf annotate. org.graalvm.buildtools native-maven-plugin -g -O0 After that, add the following Dockerfile to the project (the discussion of its contents is below): FROM bellsoft/liberica-native-image-kit-container:jdk-21-nik-23.1.3-stream-musl as builder RUN apk add libstdc++ freetype gdb libstdc++-dev freetype-dev WORKDIR /app ADD spring-petclinic-main /app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw -Dcheckstyle.skip -Dmaven.test.skip -Pnative native:compile EXPOSE 8080 EXPOSE 2345 ENTRYPOINT ["gdbserver", ":2345", "/app/spring-petclinic-main/target/spring-petclinic"] Let’s see what is happening here: First, we take Liberica Native Image Kit Container with Liberica Native Image Kit for JDK 21 and Alpaquita Linux as a base image for generating a native image; Then, we add the required packages from the Alpaquita repository: libstdc++ and libstdc++-dev for compiling C++ code, freetype and freetype-dev for font rendering, and gdb with the GDB Debugger; After that, we build a native image using the Maven plugin; Finally, we use the gdbserver command to launch the application in the debug mode (the gdbserver will listen on the specified port) and expose two ports, one for the application (8083), and another one for the debugger (2345). Note that in this case, for simplicity's sake, we don’t copy the resulting binary into a fresh base image, meaning that our Docker image contains the whole project plus the binary and generated source files. As an alternative, you can store source files and symbols locally and deploy only the binary file. Now, we can build the image with the standard docker build command: docker build -t petclinic-native-debug . -f Dockerfile-ni-debug Check the images docker images REPOSITORY TAG IMAGE ID CREATED SIZE petclinic-native-debug latest 24efe11fc544 11 seconds ago 1.63GB As you can see, the native image with the debug information included takes up 1.63GB, but as we are not going to use it in production, it is not critical. Let’s run the image with the following flags: Three essential flags making it possible to debug the app in a container. If you are using a Linux distro with a kernel version older than 5.9, you can use the --priviledged flag instead of these options, but it is not recommended running the containers with elevated permissions: --cap-add=SYS_PTRACE makes it possible to debug the app inside the container; --security-opt seccomp=unconfined enables running a container without the default Secure computing mode (seccomp) profile; --security-opt apparmor=unconfined enables running a container without the AppArmour security profile. The remaining options help us to configure Docker settings: --rm removes the container after it has been stopped; -it opens an interactive container instance; --name specifies the name of our container; -m sets the memory limit for the container; -p specifies the ports for the container. docker run --rm -it --name spring-debug -m 1g -p 8083:8080 -p 2345:2345 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined petclinic-native-debug Process /app/spring-petclinic-main/target/spring-petclinic created; pid = 9 Listening on port 2345 That’s it! Your native image is ready for the debug session! Debug native images in Eclipse IDE With the setup described in the previous section, you can perform native image debugging from the console with GDB. An alternative approach is to use the IDE. For that purpose, you need to run the Docker container as described above and connect to it from the IDE using its IP address. Let’s see how we can do that in Eclipse. Note that you can do that only on x86 systems as the local GDB installation is required, and there are no builds for Aarch64 yet. If your build and host environments are both located on a local machine, you can first generate a native executable of our project on a local machine so that the IDE has a C++ application to work with. You can do that by downloading Liberica Native Image Kit for your platform. After the installation, go to the project root directory and run the following command : JAVA_HOME=/Path/To/NIK/Home mvn -Dcheckstyle.skip -Dmaven.test.skip -Pnative native:compile The native image will be generated in the target directory. Now,, we can move on to setting up the IDE. Firstly, you need to install the Remote C++ Debugging plugin for Eclipse. After that, export the Spring Petclinic project into Eclipse. Right click on the project, select Properties → Project Natures. Add C++ Nature and C Nature. Add a debug configuration to connect to a remote host where our container is running. Select Debug Configurations… → C/C++ Remote Application. Add the spring-petclinic project in the Main tab. Then, select the Connection tab in the Debugger tab, and fill in the connection details (port number and IP address or hostname). For instance, if the container is running on localhost, leave localhost, otherwise, fill in the IP address. Remember that our debugger is listening on port 2345. Click Apply. Click Debug. Conclusion In this article, we discussed how to set up remote native image debugging. We learned how to prepare a Docker container image with a native executable for the debugging session and how to set up native image debugging in Eclipse IDE. In the following article, we will discuss how to debug native images from Intellij IDEA. Subscribe to our newsletter so as not to miss it! - [How to Debug GraalVM Native Image in IntelliJ IDEA](https://bell-sw.com/blog/how-to-debug-graalvm-native-image-in-intellij-idea/): In the previous article, we discussed how to set up a remote native image debugging session by creating a Docker container with debug information in a Docker container. We also looked into debugging native images in Eclipse IDE. Please read the article first to familiarize yourself with the theory. Starting with IntelliJ IDEA 2024.3, you can also debug native images from IntelliJ IDEA Ultimate. All you need is to install the plugin and follow the instructions in this article! Table of Contents Considerations when debugging native images from IntelliJ IDEA How to set up native image debugging in IntelliJ IDEA Prerequisites Local debugging on Linux Remote debugging session Conclusion Considerations when debugging native images from IntelliJ IDEA The GraalVM Native Debugger plugin is implemented over the GDB protocol, meaning that there are certain limitations compared to the standard debugging experience (see below). In addition, there is an issue of mixing several sources for one binary function. This issue stems from the nature of AOT compilation and can be minimized by reducing the number of optimizations. Other important factors to consider are: GraalVM Native Debugger plugin is only available in IntelliJ IDEA Ultimate Edition and the smooth experience will be on Linux. But if you use other operating systems, you can debug native images in containers. So, the tutorial below is applicable to all OSs as we build and run native images in containers. Local debugging with this plugin is possible on Linux only currently. Remote debugging (provided that target OS is Linux) is available for all operating systems via SSH or Docker Compose. If you want to debug a native image remotely, gdbserver must be installed and running on the target environment. With GraalVM Native Debugger, it is possible to step through a program in the Java source code or machine code. For more information on GraalVM Native Image debugging, refer to the documentation. How to set up native image debugging in IntelliJ IDEA Prerequisites GraalVM Native Debugger plugin installed; Docker up and running. Local debugging on Linux Let’s first look at setting up a debugging session if you are running a native image from IntelliJ IDEA and use Linux. First of all, we need to build two images. The first Dockerfile (Dockerfile-ext) is for building and debugging the native image: FROM bellsoft/liberica-native-image-kit-container:jdk-17-nik-23-musl RUN apk add gdb This Dockerfile enables you to meet the requirements to debug a native image on a local machine: you have to have GraalVM and gdbserver available in the resulting image specified as the target. Another Dockerfile is for simply running and debugging the native image: FROM bellsoft/alpaquita-linux-base:stream-musl RUN apk add gdb Build the first image: docker build . -t debug-native-runtime-ext -f Dockerfile-ext Build the second image: docker build . -t debug-native-runtime -f Dockerfile In the run widget, click Edit. Click + (Add New Configuration) in the toolbar, then select GraalVM Native Image. In the case of this configuration, IDE can build and run your application. First of all, let’s specify the name of our configuration: in our case, it is Petclinic-cmd. Then, choose where you are going to run our configuration. In our example, we run it on Docker. Select Run on Docker, and then click on Manage targets. Select Pull or use existing, which means that the image will be pulled from the local repository. Specify the image tag of the image we built previously (debug-native-runtime-ext:latest) and run options. For instance, you can specify the port. After setting up the target, specify the path to your native executable in a container. Finally, specify the module path so that the source files can be found. Now, you can start a debugging session by choosing the Native Image configuration in the run widget and clicking Debug. After that, the debugging workflow is familiar: you can set breakpoints, work with expressions, etc. Remote debugging session If you want to debug a native executable running remotely, you must first start a containerized native image with debug information and gdbserver built as specified in this article. docker start native-image-debug Then, log into the container: docker exec -it native-image-debug sh And start the application manually in the container: $ cd path/to/executable $ ./your-app After that, log into the container again and run ps to find out the PIDs of the process. Then, start the gdbserver with the PID of the running native image: gdbserver --attach :2345 PID Now, we have a gdbserver debugging our native image in a remote container. And we can port forward and have a port on our localhost available for the IDE. Now, you can set up the GraalVM Native Attach run configuration. In the case of this configuration, we attach to the process through the port, the user is responsible for starting the gdbserver on the target. The IDE is not aware of what is happening on the target. In the run widget, click Edit. Click + (Add New Configuration) in the toolbar, then select GraalVM Native Attach. Give a name to your configuration; in our case, it is petclinic-native-attach. Specify the host and port for attaching to the running process. Finally, specify the classpath of the module. After that, click Debug in the same window. Conclusion In this article, we look at several approaches to setting up the Native Image debugging session in IntelliJ IDEA. In reality, there are more scenarios and approaches to implementing them. The plugin is currently under active development, its functionality is being enhanced. We will update the article with the changes. - [2024 in Review: BellSoft’s Key Milestones and What’s Next in 2025](https://bell-sw.com/blog/2024-in-review-bellsoft-s-key-milestones-and-what-s-next-in-2025/): For all of us at BellSoft, 2024 was a year of valuable collaborations, memorable engagements, and remarkable accomplishments. As we continue to innovate and evolve, we are proud to share the milestones we’ve reached, the lessons we’ve learned, and the exciting initiatives we have ahead. In this article, I’ll walk you through the key achievements of 2024 and give a glimpse of what we’re planning for 2025. 2024: A Year of Milestone Releases Committed to Providing the Most Complete Java Experience At BellSoft, our mission has always been clear: to provide developers and enterprises with the most complete Java experience. This translates into empowering them with the tools they need to unlock the full potential of Java applications—on any platform, with any JDK version. In 2024, we reached key milestones in this journey: Adding Support for RISC-V: Recognizing the rapid growth of RISC-V, we expanded Liberica JDK to support this innovative and rapidly evolving architecture. Adding Support for 64-bit Arm: Both Alpaquita Linux and Liberica JDK Performance Edition now fully support AArch64, ensuring enhanced performance and compatibility on Arm-based hardware. Committed to Bringing Better Performance to All Java Versions At BellSoft, improving Java performance is a cornerstone of our philosophy. In 2024, we achieved a major milestone: completing our suite of performance solutions for every Java version. For JDK 11 and older: In 2023, we introduced Liberica JDK Performance Edition for JDK 11. This year, we expanded the line with Liberica JDK Performance Edition for JDK 8 - a fully open-source fused JDK that integrates JVM 17 into JDK 8 and 11. This solution delivers up to 10-15% performance improvements by leveraging JVM 17 features.Modern garbage collectors like ZGC and Shenandoah GC significantly reduce latency, while the default G1 GC contributes to better throughput and lower latency. Enhanced zlib enables super-fast compression and decompression. All of this is possible without changing your stack or dependencies, while staying 100% open-source. For JDK 17 and later: Developers using the latest Java versions can now benefit from: Liberica Native Image Kit: A GraalVM-based tool for creating native executables and bringing the startup time to near zero. Coordinated Restore at Checkpoint (CRaC) Project: This year, we extended CRaC support to Alpaquita Containers, offering containerized Java applications a ready-to-use solution for dramatically reducing startup and warmup times to milliseconds Strong and Steady Growth 2024 was a fruitful year for BellSoft’s business. Our innovative releases and strategic business approach translated directly into outstanding growth. The number of our customers doubled, and the revenue of Liberica JDK increased twofold, compared to the previous year. We are excited to see our products gaining more traction in 2024: Liberica JDK saw its user base double this year. Alpaquita Containers experienced an even more impressive increase, tripling its user base, and surpassing 1 million downloads from Docker Hub! Alpaquita Linux, our minimal Linux distribution for containers, celebrated its second anniversary, achieving a 50% growth in its user base. These milestones reflect the growing trust in our solutions and the increasing adoption of BellSoft technologies worldwide. Growing Our Developer Advocacy Team As our community continues to grow, so does our team of Developer Advocates. Our Performance Architect, Dmitry Chuyko, has been leading BellSoft’s Developer Relations from the very beginning. This year, our Developer Advocacy team expanded with two new members: Pasha Finkelshteyn, an experienced software engineer and Advocate for Java and Kotlin, and Catherine Edelveis, who has been with BellSoft for several years and recently stepped into the limelight as part of our Developer Advocacy team. With this expansion of our DevRel team, we continued strengthening the connection with the community, bringing renewed energy. As part of this, we relaunched our YouTube channel, where Catherine now posts weekly video tutorials and guides. We invite you to subscribe and stay updated with our content. Additionally, you may have seen our DevRel team on various livestreams and webinars, where we foster collaboration with our partners, friends, and colleagues, and continue building meaningful connections within the community. Sharing Knowledge Globally We love exchanging knowledge and experiences with the Java community, and this year, the conversation was truly sparking! Our team attended many conferences across the globe, the most memorable of them being: Devoxx Belgium, Spring I/O, and KubeCon + Cloud Native Con in Paris, Hong Kong, and Salt Lake City. But that’s not all. For those who can’t attend conferences or local JUG meetups, we continue sharing knowledge through our blog, regularly publishing new articles on Java. As mentioned earlier, we’re also adding video content to engage our community in fresh and dynamic ways. I encourage you to reach out to our team and let us know on what topics you’d like us to create videos or articles in 2025. You can find us on all major social media platforms. Follow us on X: https://x.com/bellsoftware Bluesky: https://bsky.app/profile/bellsoft.bsky.social LinkedIn: https://www.linkedin.com/company/bell-sw Key Challenges and Trends in Java Development At Devoxx Belgium, BellSoft conducted the Java Developer Survey to explore key challenges and trends in Java development. The results showed that a large number of developers see room for improvement in Java application performance, with 73% believing it could be enhanced. Additionally, 58% pointed out that business managers often overlook Java's potential to reduce cloud costs. The survey also revealed a growing trend in AI tool usage for code writing, with 74% of developers adopting these technologies. Moreover, 18% highlighted the increasing importance of "green Java.” You can read the full report here. Some of the findings are surprising and insightful, offering a deeper understanding of the current needs of Java developers. Looking Forward to 2025 In 2025, Java will celebrate its 30th anniversary. Despite its age, Java continues to be one of the most popular programming languages, proving its enduring relevance in the ever-evolving tech landscape. At BellSoft, we remain fully committed to driving innovation, improving the developer experience, and making development and deployment easier and faster. I would like to thank our partners, customers, and users for their continued trust and support. Together, we’ve accomplished so much, and we’re excited to see what we’ll achieve in the coming year. I’m looking forward to 2025! - [Java Adventures 2024: From Belgian Chocolate to Double Dinners](https://bell-sw.com/blog/java-adventures-2024-from-belgian-chocolate-to-double-dinners/): As I look back at 2024, I can't help but smile. From accidentally attending two conference dinners in one night to hiking in snowy Utah while locals sunbathed, from sparking new IDE features to connecting with Java communities worldwide – these are my adventures as a part of the BellSoft Developer Advocate team. As Tradition Goes: Another Year Begins in Brussels As every year, 2024 started with the FOSDEM and OpenJDK Committers’ Workshop in Brussels. There's something special about starting the year in this city – perhaps it's the unique charm of my familiar walking routes, or maybe it's just the chocolate. Speaking of which, I've scientifically proven (through multiple rigorous trials) that Belgian chocolate makes for a perfectly acceptable lunch substitute. Looking forward to continuing these "research studies" in 2025! Spring Beginnings: London Calling and Amsterdam Dreams My year of travels continued in May with Devoxx UK. Being my first time in London, I had to tick off some bucket list items – and yes, riding a double-decker bus was absolutely one of them! Between sessions, I managed to channel my inner detective with a visit to Hercule Poirot's house. Who says tech conferences can't have a dash of mystery? Amsterdam followed, where the JetBrains office proved to be as impressive as their IDEs. Meeting old friends there was a highlight – what started as quick catch-ups turned into conversations lasting for hours. The European leg of my journey wrapped up in Cologne, another city that holds a special place in my heart. Though I have to report a serious regression in this year's JCON conference – the traditional Bratwurst sausages were mysteriously absent from the menu. If any conference organizers are reading this, I can assure you that Java performance isn't the only thing that benefits from proper optimization! After all, some hot meat is needed to focus on garbage collection discussions. The Tale of Two Dinners in Barcelona Spring I/O in Barcelona marked one of my most memorable conference experiences, though perhaps not for the reasons you'd expect. Picture this: I show up for what I think is the speaker's dinner, enjoy some great conversations, only to realize I'm at the wrong dinner! Being the dedicated speaker that I am, I headed to the actual speaker's dinner afterward. The conference itself was a huge success for BellSoft. Both Pasha (whom I finally met in person!) and I delivered talks, and our booth buzzed with Spring developers. Learning that many developers are embracing the latest Java versions was a pleasant surprise – though perhaps not as surprising as my accidental dining adventures. Before Spring I/O, I had the pleasure of speaking at the Barcelona JUG, and what an experience that was! The JUG was hosted in an office with a stunning terrace overlooking Sagrada Familia – not your everyday tech talk backdrop. Speaking of Sagrada Familia, I made sure to visit Gaudi’s masterpiece properly. Having learned from past adventures, I booked my tickets well in advance – sometimes being a planner has its perks! The magnificent architecture left me speechless, though I did manage to take plenty of photos. American Adventures: Coast to Coast August brought me to the sunny shores of California, where Pasha and I dove into the JVM Language Summit. There's nothing quite like being surrounded by people who get just as excited about JVM internals as you do! The summit was packed with insights, and being able to visit our San Jose HQ afterward made it even more special. There's something irreplaceable about those face-to-face conversations with colleagues you usually only see in video calls. The trip took an East Coast turn as I headed to New York, where I had the pleasure of speaking at the New Jersey JUG. The venue alone was worth the trip – New Jersey Drew University's beautiful old-fashioned building had me seriously contemplating a return to student life. There's something inspiring about discussing modern technology while surrounded by classical architecture. This is not my first time in New York, but the city never fails to surprise me with its scale. I attempted to explore the Metropolitan Museum, but quickly realized one day isn't nearly enough – it's enormous and packed with incredible art! Definitely need to come back for more visits. The Great Native Image Debugging Plot Twist Here's a story of how one conference talk led to an unexpected feature in IntelliJ IDEA. Back in London, I gave a presentation about debugging native images using Eclipse IDE, as it was the only one supporting this feature at the time. Little did I know that JetBrains' team lead Alexey Stukalov was in the audience. He reached out after the talk, curious about bringing this capability to IntelliJ IDEA, and we had many discussions about features and workflows useful for developers. Several months and many discussions later, I found myself at the GraalVM Summit in Zurich, scheduled to present with Alexey. The twist; JetBrains had just finished developing their updated native image debugging plugin – literally the night before our talk! We spent the evening rewriting our presentation to showcase this brand-new feature. From discussing "what could be" in London to demonstrating "what is" in Zurich – that's what I call an exciting plot twist! Between sessions, we managed to tour Zurich with a tour guide none other than Thomas Wuertinger himself. I visited Einstein's locker at ETH Zurich (no quantum equations were harmed in the process), and a chocolate factory. I must confess I might have behaved like Augustus Gloop from Charlie and the Chocolate Factory. But hey, wouldn't we all? The UK Tour: A Test of Transportation and Tenacity October brought what I like to call my "Three Cities, Three Days, Three JUGs" challenge. Starting in Brussels (where chocolate is an acceptable lunch), I navigated through Lille, Oxford, and Manchester. Despite the British transport system's best efforts to destroy my plans with delays, I managed to make every single talk. The secret? Always plan for delays, and never underestimate the power of a backup for your backup plan! Final Adventure: KubeCon in Salt Lake City My year wrapped up at KubeCon in Salt Lake City, where our team faced an unexpected challenge when one member fell to the flu, leaving just the two of us to manage the booth. But the real adventure came after the conference, when I went hiking in near-freezing temperatures. Fully equipped in my warmest gear, I felt slightly overdressed when I encountered locals jogging in shorts and – believe it or not – sunbathing in swimsuits! My symbolic hike to Bells Canyon (a nod to BellSoft) and trek up to Ensign Peak offered stunning views and a perfect opportunity to reflect on what a remarkable year it had been. Looking Back Looking back, 2024 taught me that the best moments often happen off-script – from delayed trains leading to unexpected city explorations to conference talks sparking surprising collaborations. As I look forward to 2025, I'm grateful for every person who made this journey memorable, every JUG that welcomed me, and yes, every piece of Belgian or Swiss chocolate that substituted for lunch. Here's to another year of adventures and innovation! - [How to profile Java applications in Docker containers](https://bell-sw.com/blog/how-to-profile-java-applications-in-docker-containers/): Profiling the containerized application is essential for monitoring the behavior of the program. It also helps you to promptly pinpoint the root causes of possible performance issues. In my previous article, I looked at different profiling tools for Java applications. In this article, I will use one of them, Java Flight Recorder to work with containerized workloads. In the following blog post, I will show you how to use Async Profiler to profile Java apps in containers. JFR, which is an integral part of JVM, is a low-overhead profiler and fairly easy to use. We will look into two approaches to profiling the containerized application: continuously from the application start and an arbitrary point of time. I will also show how to profile applications if you use buildpacks. Feel free to jump to the section most relevant to you. Table of Contents Profiling in containers: basic concepts Setting up the environment for profiling Profiling Docker containers with Java Flight Recorder Use JFR in a Docker container at application start Use JDK Mission Control with Docker containers Use ephemeral containers to profile an application with JFR and jcmd Profiling containers with JFR and buildpacks Conclusion Profiling in containers: basic concepts There are three key concepts related to profiling containerized applications we need to understand. We have a target environment, which is our containerized application, and a host environment, which is a developer machine used for controlling the profiling process and analysing the resulting profiling data. We also have a profiling tool: it can be part of a target container or attached to it externally. JFR is part of JVM and so it is always included into the target environment. Profiling Java applications in containers Target and host environments can be located on one or two machines. There are two approaches to profiling the application. You can perform continuous profiling right from the application start. In this case, the profiler will be a part of the target environment, namely the container image with the application. If you use a third-party profiler, you need to add it to the container image. However, in most cases, we don’t want to profile the application continuously, but rather attach to it, take a few samples, and see how the app is doing at this moment. In this case, the profiler will be in a separate environment, namely the ephemeral container. Note that we can’t detach the JFR from the JVM, so it will remain in the target container. However, we can control it with the help of jcmd, which is part of the JDK. So, the ephemeral container will be based on a full Java Development Kit with jcmd. If you use a third-party profiler, you can also add it to the ephemeral container. The ephemeral containers are started at an arbitrary point of time and run temporarily to accomplish some task: for instance, perform the debugging or profiling session. These containers are useful when you deploy distroless containers that don’t contain a shell, debugging utilities, or profiling tools. Setting up the environment for profiling For this tutorial, I will use a reference Spring Boot application, Petclinic. There are two ways to containerize the application: using a Dockerfile or buildpacks. I will show you the work process with both approaches. To build a container, I will use Liberica Runtime Container based on Liberica JDK Lite optimized for the cloud and Alpaquita Linux. Liberica JDK is recommended by Spring and is used by default in Spring buildpacks. Profiling Docker containers with Java Flight Recorder Use JFR in a Docker container at application start You can start a JFR recording right at application startup. JFR is part of JVM, so you don’t have to add it manually. If you use Dockerfiles, you need to specify the -XX:StartFlightRecording option in the Java command line with a set of necessary arguments separated by comma. You can also specify the -XX:+UnlockDiagnosticVMOptions and -XX:+DebugNonSafepoints to make the profiling more accurate: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder WORKDIR /app ADD spring-petclinic-main /app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw clean package FROM bellsoft/liberica-runtime-container:jre-21-stream-musl WORKDIR /app EXPOSE 8081 ENTRYPOINT ["java", "-jar", \ "-XX:+UnlockDiagnosticVMOptions", \ "-XX:+DebugNonSafepoints", \ "-XX:StartFlightRecording=duration=30s,filename=/tmp/recording.jfr", \ "-XX:MaxRAMPercentage=80.0", "/app/petclinic.jar"] COPY --from=builder /app/spring-petclinic-main/target/*.jar /app/petclinic.jar Here, we have set the recording time to 30 seconds. But you can let JFR run silently in the background. As JFR is a low-overhead profiler, continuous profiling won’t significantly impact the application performance. To do that, you need to specify -XX:StartFlightRecording=name=background,maxsize=100m The maxsize is the limit to the events stored in memory. Then, start the container the usual way: docker run -p 8082:8080 --name pet petclinic The recording will be in the /tmp directory of the container. To copy the file to the current directory, run docker cp: docker cp :/tmp/recording.jfr . Use JDK Mission Control with Docker containers You can observe the running Java application and receive profiling data in real time using JDK Mission Control. First of all, you need to install JDK Mission Control. It is not part of Java runtimes, but you can download it separately from vendors that provide it. For instance, you can get Liberica Mission Control. Next, to connect to a remote JVM you need to configure the Java Management Extensions (JMX) with the following JVM environment variables: -Dcom.sun.management.jmxremote to enable the monitoring from a remote system; -Djava.rmi.server.hostname to specify the host. Set 127.0.0.1 for localhost; -Dcom.sun.management.jmxremote.rmi.port for the RMI port and -Dcom.sun.management.jmxremote.port for the JMX port. Both must have the same value; -Dcom.sun.management.jmxremote.authenticate and -Dcom.sun.management.jmxremote.ssl to set JMX authentication and SSL if required. Set to false if not needed. Specify these options in the ENTRYPOINT in your Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder WORKDIR /app ADD spring-petclinic-main /app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw clean package FROM bellsoft/liberica-runtime-container:jre-21-slim-musl WORKDIR /app EXPOSE 8082 ENTRYPOINT ["java", "-jar", "-Dcom.sun.management.jmxremote", \ "-Djava.rmi.server.hostname=127.0.0.1", \ "-Dcom.sun.management.jmxremote.rmi.port=7091", \ "-Dcom.sun.management.jmxremote.port=7091", \ "-Dcom.sun.management.jmxremote.authenticate=false", \ "-Dcom.sun.management.jmxremote.ssl=false", \ "-XX:MaxRAMPercentage=80.0", "/app/petclinic.jar"] COPY --from=builder /app/spring-petclinic-main/target/*.jar /app/petclinic.jar Build the container and start it. Note that you need to specify ports both for the application and the connection ports: docker run -p 8082:8080 -p 7091:7091 --name pet petclinic-jmx Note that instead of port mapping you can use the host name that Docker generates for each container. After starting a container with enabled JMX, you need to connect to the remote JVM. Open Liberica Mission Control, select New Custom JVM Connection, and specify the host name and port: After that, you can connect to the JVM in your container. Right click on the created connection and select Start JMX Console: Use ephemeral containers to profile an application with JFR and jcmd The scenario we discussed above was fairly simple to implement. But what if you want to attach to a running container at an arbitrary point of time? In this case, you will need jcmd to control the JFR. It is a utility that sends diagnostic commands to the JVM. The jcmd tool is part of JDK, but we don’t want to use a JDK base image for running the application and increase the size of the container, do we? The solution is to use ephemeral containers. Containerize the application without any additional JFR-related options. Then, build another container image based on the following Dockerfile: FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl This will be our ephemeral container. It is based on Alpaquita Linux and Liberica JDK containing jcmd. Run the containerized application with a standard command: docker run -p 8082:8080 --name pet pet-prof-jfr To attach the ephemeral container, we need to place it in the same PID namespace as our application with --pid=container:. We also need the -it flag to run it in the interactive mode. Note that Docker containers restrict the access to perf_event_open syscall. So, to gain access to the JVM performance information, you need to specify the --cap-add SYS_ADMIN capability when starting the container. You may also need to modify the seccomp profile or disable it with --security-opt seccomp=unconfined. docker run --cap-add SYS_PTRACE --security-opt=apparmor:unconfined --name ephem --pid=container:pet -it prof-jcmd sh Cool! We are inside the ephemeral container. From here, run the jcmd command to find out the PID of the Java application running in a container we attached to: /app # jcmd 1 /app/petclinic.jar 62 jdk.jcmd/sun.tools.jcmd.JCmd Finally, you can start a JFR recording with the jcmd command specifying the PID of the process, 1 in this case. You can also configure the recording as you see fit. For instance, you can set the duration and filename (the full list of options can be found in the docs): /app # jcmd 1 JFR.start duration=15s filename=/tmp/recording.jfr 1: Started recording 1. The result will be written to: /tmp/recording.jfr After the profiling is complete, you can retrieve the recording from the target container with docker cp: docker cp :/tmp/recording.jfr . Profiling containers with JFR and buildpacks First, let’s look at the scenario where you want to start the profiling session right at the application start. In the case of buildpacks, build a container image without any additional configuration. Maven: mvn spring-boot:build-image Gradle: gradle bootBuildImage Then, specify the following environment variables when starting a container: BPL_JFR_ENABLED set to true to enable JFR; BPL_JFR_ARGS (optional) to specify a list of necessary arguments separated with a comma. If you don’t specify this variable, the default JFR settings will be used: dumponexit=true and filename=/recording.jfr. An example command for enabling JFR in a container built with buildpacks: docker run --env BPL_JFR_ENABLED=true --env BPL_JFR_ARGS=filename=/tmp/jfr-recording.jfr,duration=15s -p 8082:8080 --name pet petclinic The recording will be in the /tmp directory of the container. You can get it with: docker cp :/tmp/recording.jfr . What if you want to monitor the application in Mission Control? If you use buildpacks, you should use the BPL_JMX_ENABLED environment variable set to true when starting the container. The following settings will be provided to the JVM by default: -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.rmi.port=5000 You can also use the BPL_JMX_PORT environment variable to change the port. An example command for enabling JFR in a container built with buildpacks: docker run -e BPL_JMX_ENABLED=true -e BPL_JMX_PORT=5001 -p 8082:8080 -p 5001:5001 --name pet petclinic If you need to change the host name, you should provide the JMX arguments as JVM options in the buildpacks configuration INSTEAD of using the BPL_JMX_ENABLED variable. In Maven, you can do that with BPE_APPEND_JAVA_TOOL_OPTIONS: -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=192.33.22.11 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=5001 -Dcom.sun.management.jmxremote.rmi.port=5001 And then, start the container: docker run -p 8082:8080 -p 5001:5001 --name pet petclinic You can now connect to the remote JVM via Java Mission Control (see the instructions in the Use Java Mission Control Section). And finally, if you want to start the recording at an arbitrary point of time, you can use jcmd and ephemeral containers as described in the Section above. Conclusion In this article, we looked into profiling containerized Java applications with Java Flight Recorder. As JFR is part of JVM, using it is fairly easy. You can profile the application at start up or attach an ephemeral container with jcmd to profile the app at any time. In the following blog post, we will see how to profile Java apps with Async Profiler, another low-overhead open-source Java profiler. Subscribe to our newsletter so as not to miss it! - [Liberica JDK 8u442, 11.0.26, 17.0.14, 21.0.6, and 23.0.2 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u442-11-0-26-17-0-14-21-0-6-and-23-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u451, 7u451, 8u441, 11.0.25.0.1, 17.0.13.0.1, 21.0.5.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u442, 11.0.26, 17.0.14, 21.0.6, and 23.0.2 with non-critical fixes and general improvements. The release contains 847 fixes and backports overall. BellSoft participated in eliminating 6 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 1 security issue (CVE) fixed. 30 total security fixes (+ 2 additional non-security fixes) in CPU release: in Liberica 6u451: 5 security fixes + 1 additional fixes; in Liberica 7u451: 5 security fixes + 1 additional fixes; in Liberica 8u441: 5 security fixes; in Liberica 11.0.25.0.1: 5 security fixes; in Liberica 17.0.13.0.1: 4 security fixes; in Liberica 21.0.5.0.1: 4 security fixes. In addition, PSU releases include a total of 817 fixes and backports: in Liberica 8u442: 5 security fixes (+ 2 in FX) + 23 additional fixes (+ 9 in FX); in Liberica 11.0.26: 5 security fixes (+ 2 in FX) + 23 additional fixes (+ 9 in FX); in Liberica 17.0.14: 4 security fixes (+ 2 in FX) + 258 additional fixes (+ 15 in FX); in Liberica 21.0.6: 4 security fixes (+ 2 in FX) + 233 additional fixes (+ 13 in FX). in Liberica 23.0.2: 4 security fixes (+ 2 in FX) + 189 additional fixes (+ 13 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2025-21502 4.8 hotspot compiler network high none none unchanged low none none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 23 CVE-2025-21502 𑇐 𑇐 𑇐 𑇐 𑇐 Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Liberica Native Image Kit 23.0.7, 23.1.6, and 24.1.2 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-7-23-1-6-and-24-1-2-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.7 for JDK 17, 23.1.6 for JDK 21, and 24.1.2 for JDK 23 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Liberica NIK 23.0.7 is based on Liberica JDK 17.0.14, NIK 23.1.6 is based on Liberica JDK 21.0.6, and NIK 24.1.2 is based on Liberica JDK 23.0.2. Notable improvements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2025-21502 4.8 hotspot compiler network high none none unchanged low none none Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica Native Image Kit - [A Comprehensive Guide to Mutation Testing in Java](https://bell-sw.com/blog/a-comprehensive-guide-to-mutation-testing-in-java/): Testing the code means verifying that the code works correctly and as expected. But how do you know the tests evaluating your code are good enough or provide adequate coverage? This is where mutation testing comes into play. Mutation testing helps assess the quality of a test suite by verifying whether your tests are effective and can identify bugs as they are meant to. It is especially useful for enterprise Java applications with thousands of unit tests as it enables developers to quickly identify code spots not covered by tests. In this article, we will discuss the key concepts, metrics, and best practices of mutation testing and learn how to perform it for Java applications using the PITest library. The code used in this article is available on GitHub. Table of Contents What is mutation testing? What is a mutation Types of mutation Key metrics of mutation testing Why you should use mutation testing Advantages and disadvantages of mutation testing Best practices of mutation testing How to perform Java mutation testing with PITest What is PITest Set up the project Run mutation tests Analyze the results of mutation testing Improve the mutation score Configure PITest Conclusion FAQ What is mutation testing? Mutation testing is a technique that introduces bugs into the code to see whether the existing tests can identify these bugs. If none of the tests fails after introducing a bug, it means that it was not detected and the test suite is inadequate. Mutation testing shows which parts of the code are not covered by tests, but it is not the same as line coverage. Line coverage is part of the code coverage metrics that measures the percentage of code lines executed during the tests, but it doesn’t say anything about the quality of tests. There’s also another concept called branch coverage that measures the number of branches executed, most typically, if statements. Mutation coverage is closer to the branch coverage, but gives even more information. Let’s look at the following example. We have a simple Task class: @Entity @Table(name = "tasks") public class Task { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", length = 150) private String name; @Column(name = "start_time") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime start; @Column(name = "end_time") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime end; } In the TaskService class, we have only one method for checking whether there is an overlap of dates for a new task with dates for some existing task: public boolean isOverlap(LocalDateTime newStart, LocalDateTime newEnd, Task task) { return !(newEnd.isBefore(task.getStart()) || newStart.isAfter(task.getEnd())); } Suppose we have written this test to verify that it functions correctly: @Test void shouldFindOverlap() { Task task = new Task( "Make lunch", "2025-02-01 12:00", "2025-02-01 12:30"); LocalDateTime newStart = DateTimeParser.parseToLocalDateTime("2025-02-01 11:00"); LocalDateTime newEnd = DateTimeParser.parseToLocalDateTime("2025-02-01 12:15"); assertTrue(taskService.isOverlap(newStart, newEnd, task)); } So far, we have 100% line coverage (but not 100% branch coverage) for TaskService as we have executed all lines of the one method we have. But this is by no means a good test suite. We don’t know what happens if there’s no time overlap. We also can’t be sure that if somebody introduces changes to the code, the test will fail. Another example of good line coverage with poor-quality tests: @Test public void testThatDoesNothing() { // Create a mock object TaskService mockService = mock(TaskService.class); // Set up a method to do nothing when called doNothing().when(mockService).deleteTask(1L); // Call the method mockService.deleteTask(1L); // Verify that the method was called verify(mockService.deleteTask(1L)); } There’s an error in this test. If we wrote verify(mockService).deleteTask(1L), then the test would actually check the code. But with verify(mockService.deleteTask(1L)), it does nothing, so it will always pass! Such a mistake is easy to make, so there is a non-zero risk that such specimens could nest among hundreds or thousands of unit tests for an enterprise application. Mutation testing aims to reveal such weak spots in your tests by introducing mutations. What is a mutation A mutation is a slight change made to the program’s code to introduce a bug or regression. These altered code versions are called mutants. After the mutations have been inserted, the mutated versions of the program are run with the existing test suite. If tests fail, it means that the mutant was killed. If tests pass, the mutant has survived. In the latter case, the test suite should be improved to kill the remaining mutants. In most cases, it means you have to add the missing tests for the code that wasn’t tested. Note that mutation testing does not aim to verify the quality of the code. Its goal is to assess the quality of the test suite. Types of mutation Mutants are typically very small and can be divided into several types: Statement mutations: statements can be changed or removed. For example, int sum += i becomes int sum = i; Value mutations: a value is changed. For instance, int age = 18 becomes int age = 16; Operator mutations: arithmetic or logical operators are changed. For instance, TRUE becomes FALSE or a > b becomes a < b. Let’s look at the code snipped with the isOverlap() method again. If we replace boolean return with TRUE like that: public boolean isOverlap(LocalDateTime newStart, LocalDateTime newEnd, Task task) { return true; } We introduced a mutation manually as an example. Of course, you shouldn’t create mutants yourself or change the production code. There are tools that we will discuss in more detail below that will automatically create copies of the code with mutations and provide detailed reports on the testing results. Key metrics of mutation testing Mutation score is used to measure the effectiveness of the test suite. The mutation score is the number of killed mutants divided by the total number of mutants multiplied by a hundred: The mutation score of 100% means that all mutants were killed, and the test suite is effective. It’s important to note that sometimes, we can encounter equivalent mutants among other mutations. Equivalent mutants are changes to the program syntax that behave like the original program. For instance, look at the following snippet: public int compare(Item item1, Item item2) { if (item1.price > item2.price) { return 1; } else if (item1.price < item2.price) { return -1; } else { return 0; } If we change the return value from 1 to 2 (or from -1 to -2), the program’s behavior will not change because the Comparator returns a negative integer, zero, or a positive integer, so it doesn’t matter which positive or negative number to return. Equivalent mutants can’t be killed, so you will have to either rewrite the code if they can potentially lead to bugs or not consider them in the overall mutation score. Why you should use mutation testing Mutation testing is a good but often overlooked practice that should be taken on board as an integral part of the development/testing processes for the following reasons: As demonstrated above, line coverage is not the best indicator of test quality. Mutation testing helps find flaws in your test suite. When you introduce changes to the program, mutation testing helps to see whether the tests still function correctly. When you start working on an existing project, mutation testing can help you rapidly assess the quality of its test suite. Sometimes, your program may include code that is difficult to test with unit tests; mutation testing can help you with such code. Mutation testing helps to make testing different scenarios, not just happy paths, a habit. Advantages and disadvantages of mutation testing Integration of mutation tests into the project has several advantages: Mutation tests can help you find missing tests and improve the reliability of the test suite; Enterprises can assess the quality of the existing test suite using the mutation score; Although mutation tests are not meant for checking code quality, the presence of equal mutants can, in some cases, point to potential weak spots in the code; Mutation testing helps to safely refactor the test suite after introducing changes to the code. Possible disadvantages of mutation testing: Improving the mutation score may take a lot of time, depending on the size of the application and the test suite; Mutation testing becomes an additional step in the testing process, prolonging the overall testing time; Mutation testing without a specialized tool is time-consuming. Using a tool for automatic mutation testing and integrating mutation tests at an earlier stage of project development can reduce the disadvantages. Best practices of mutation testing There are two key recommendations for efficient mutation testing: If possible, integrate mutation testing at an early stage of development to reduce the time spent on testing, assessing, and refactoring; Do not run the whole test suite with mutations every time. Use mutation tests for changed code parts to save time. Enough theory: let’s kill some mutants! How to perform Java mutation testing with PITest In this tutorial on using mutation testing for Java applications, we will use an automated tool for mutation testing. Several mutation testing systems exist for Java applications, including PITest, Javalanche, µJava, or Jumble. We will use PITest. What is PITest PITest is an open-source tool for performing mutation testing on Java and Kotlin applications. It is fast, easy to use, and targeted to real-world applications. PITest integrates with common build tools, including Maven and Gradle, JUnit, and the most popular mocking frameworks. PITest inserts mutations into the bytecode generated by the compiler, which is faster than working with source code. However, sometimes, it may be complicated to understand how mutations can be mapped to the source code. After running the tests, PITest produces easy-to-understand reports with information about line coverage and mutation coverage. Set up the project For this tutorial, we will use a small Spring Boot demo application, snippets from which you have already seen above. The code is available on GitHub. You can also take your own application, just make sure there are some unit tests. We are testing the tests, right? Let’s see how PITest works with Spring Boot. Go to Spring Initializr, select Java 23, Maven, Jar, and a couple of dependencies: H2 Database and Spring Data JPA. Generate the project and open it in your favorite IDE. Make sure that you have Java 23 installed: you can download Liberica JDK recommended by Spring. Now, let’s integrate PITest. You need to add the following plugin to pom.xml: org.pitest pitest-maven 1.17.4 org.pitest pitest-junit5-plugin 1.2.1 That’s it, no more configurations required! Nevertheless, you can configure the PITest tests as you see fit. We'll talk about it later. Our application consists of a POJO class Task: @Entity @Table(name = "tasks") public class Task { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", length = 150) private String name; @Column(name = "start_time") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime start; @Column(name = "end_time") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime end; //getters, setters, constructors An auxiliary class DateTimeParser: public class DateTimeParser { static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); public static LocalDateTime parseToLocalDateTime(String date) { return LocalDateTime.parse(date, dateTimeFormatter); } } TaskRepository: public interface TaskRepository extends JpaRepository { } And finally, TaskService with business logic, which is quite simple. We can save the task to a database, but only if it doesn’t collide with other tasks in terms of schedule: @Service public class TaskService { private final TaskRepository taskRepository; public TaskService(TaskRepository taskRepository) { this.taskRepository = taskRepository; } public boolean isOverlap(LocalDateTime newStart, LocalDateTime newEnd, Task task) { return !(newEnd.isBefore(task.getStart()) || newStart.isAfter(task.getEnd())); } protected boolean checkSlots(LocalDateTime newStart, LocalDateTime newEnd) { return taskRepository.findAll() .stream() .noneMatch(t -> isOverlap(newStart, newEnd, t)); } public int createTask(Task newTask) { if (newTask.getStart().isBefore(newTask.getEnd()) && checkSlots(newTask.getStart(), newTask.getEnd())) { taskRepository.save(newTask); return 1; } return -1; } } We already have one test in the TaskServiceTest class. Let’s add one more to verify that we can successfully save the task: @SpringBootTest public class TaskServiceTest { @Autowired private TaskService taskService; @Test void shouldFindOverlap() { Task task = new Task( "Make lunch", "2025-02-01 12:00", "2025-02-01 12:30"); LocalDateTime newStart = DateTimeParser.parseToLocalDateTime("2025-02-01 11:00"); LocalDateTime newEnd = DateTimeParser.parseToLocalDateTime("2025-02-01 12:15"); assertTrue(taskService.isOverlap(newStart, newEnd, task)); } @Test void shouldSaveTask() { Task task = new Task( "Make lunch", "2025-02-01 12:00", "2025-02-01 12:30"); int val = taskService.createTask(task); assertEquals(1, val); } } That’s it, we can now set PITest loose on our test suite! Run mutation tests To run mutation tests, you need this command: mvn test-compile org.pitest:pitest-maven:mutationCoverage The mutationCoverage goal covers all classes that match target tests and classes. PITest will automatically analyze the coverage, create mutants, and generate the reports you will find in the target directory under pit-reports. You can also use the -DwithHistory flag to speed up the analysis of the same codebase. This flag enables saving the data into a temporary directory, so it is good for local development. In other cases, you can use incremental analysis to point to the location of mutation analysis results: historyInputLocation and historyOutputLocation or for Maven historyInputFile and historyOutputFile. Analyze the results of mutation testing The pit-reports directory includes several files: index.html with a summary of testing results and files with a more detailed overview of inserted mutations into each class. Let’s first look at index.html. PITest Summary report As you can see, our mutation coverage is far from perfect and nests at 38%, meaning that 24 mutants were introduced and only 9 were killed. Well, we didn’t write any tests for the Task class, which is the main source of issues. But let’s focus on the TaskService class. The mutation score for this class is better, 64%, with 11 mutants injected and seven killed. Let’s open the Testservice.java.html file to see where these mutations lived. TaskService testing results Here’s our code highlighted with four colors: light green shows line coverage, dark green shows mutation coverage, light red shows lack of line coverage, and dark red shows lack of mutation coverage. If you scroll down a bit, you will see the summary of mutants that were killed or have survived and where they were planted. Description of mutants used in tests The report also lists types of mutations used: CONDITIONALS_BOUNDARY EMPTY_RETURNS FALSE_RETURNS INCREMENTS INVERT_NEGS MATH NEGATE_CONDITIONALS NULL_RETURNS PRIMITIVE_RETURNS TRUE_RETURNS VOID_METHOD_CALLS The full list of available mutations can be found here. Let’s try to improve our mutation score! Improve the mutation score For the sake of brevity, let’s concentrate on the TaskService class coverage. On line 29, a boolean operator was changed to false. On line 35, the conditional was negated. In addition, we have no coverage for line 39. Let’s add one more test to see if it will be able to kill the test of the mutants. This time, we’ll verify that we can’t save the task if there are no available slots. @Test void shouldNotSaveTaskIfOverlap() { Task task = new Task( "Make lunch", "2025-02-01 13:00", "2025-02-01 14:30"); int val = taskService.createTask(task); assertEquals(-1, val); } Run the mutation tests again and check the report. PITest mutant summary Great, all the mutants were killed! You can do a little exercise and improve the mutation score for the Task class. Configure PITest As mentioned above, it is recommended to run mutation tests in case of code changes. So, there’s often no need to test all classes and tests in your project: it can be very time-consuming. You can configure which classes should be mutated in the plugin: org.pitest pitest-maven 1.17.4 org.pitest pitest-junit5-plugin 1.2.1 com.myproject.somepackage.* com.myproject.otherpackage.Utils* The same can be done to tests: com.myproject.mutations.test.* You can also specify which mutators should be applied: NEGATE_CONDITIONALS MATH A full list of configuration options can be found here. Conclusion In this article, we discussed mutation testing and its value in assuring the test suite’s quality. It is an excellent supplementary technique for other testing metrics used in your company. With the help of the right tool, doing mutation testing is not difficult. We saw that as we learned how to perform mutation testing on Java applications using the PITest library. Don’t forget to subscribe to our newsletter to read more posts on best practices and cutting-edge solutions for Java development! FAQ How does mutation testing differ from standard unit tests? Unit tests are aimed at validating the expected behavior of the code. Mutation testing evaluates whether the test suite is complete enough to detect bugs in the executed code. Can I use mutation testing for enterprise projects? Mutation testing is recommended for a project of any size and purpose. What does mutation score below 100% mean? A mutation score below 100% means that not all mutants planted into the code were killed, meaning that there are flaws in the existing test suite. How to improve mutation score? In most cases, you need to add the lacking tests to improve the mutation score. - [Profiling Java Docker containers with Async Profiler](https://bell-sw.com/blog/profiling-java-docker-containers-with-async-profiler/): In the previous article, we looked at ways of profiling Java applications in containers with Java Flight Recorder (JFR), a profiling a diagnostics tool built into the JVM. In this article, I will show you how to do the same with Async Profiler, an open-source profiler. There are two approaches to profiling a containerized Java application with a third-party profiler: from within the container with the application and from the host. I will show you both techniques. Table of Contents What is Async Profiler? Use Async Profiler in a Docker container upon application start Profile Java containers with Async Profiler from host Use Async Profiler with buildpacks Best practices of profiling containerized workloads Conclusion What is Async Profiler? Async Profiler is an open-source low-overhead profiler for Java. It collects information about various JVM events, including but not limited to CPU usage, heap allocation, method invocation, lock contention. Async Profiler is easy to use, it is very small and without GUI, so it can easily be embedded into other solutions. For instance, it is used under the hood of IntelliJ Profiler. You might be familiar with the in-built JDK Flight Recorder, but how does Async Profiler differ from it? Async Profiler is a third-party profiler, so you have to download the binary to use it. If you want to use it in the container, the easiest way is to install it as a package from the Linux repository. Async Profiler can profile all calls, including native ones, and kernel functions. The data is collected in the form of flame graphs, plain text, or JFR files. Flame graphs are a hierarchical visualization of stack traces of profiled software. They can help developers quickly identify most frequently executed code paths. Async Profiler supports generation of interactive Flame graphs that accumulate data for one JVM event. Example of a Flame graph If you want to profile multiple JVM events at once, the only output format is a JFR file. JFR files can be analyzed in Mission Control. You can read more about this and other profiling tools in the Overview of top Java profiling tools. Use Async Profiler in a Docker container upon application start To profile an application with Async Profiler from a container, you need to include it into the image. It is easy to do with Alpaquita Linux as you can simply install the async-profiler package from the repository. FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder WORKDIR /app ADD spring-petclinic-main /app/spring-petclinic-main RUN cd spring-petclinic-main && ./mvnw clean package FROM bellsoft/liberica-runtime-container:jre-21-stream-musl RUN apk add async-profiler WORKDIR /app EXPOSE 8082 ENTRYPOINT ["java", "-agentpath:/opt/async-profiler/lib/libasyncProfiler.so=start,event=cpu,file=/tmp/profile.html", \ "-jar", "-XX:MaxRAMPercentage=80.0", "/app/petclinic.jar"] COPY --from=builder /app/spring-petclinic-main/target/*.jar /app/petclinic.jar Note that we are not using a “slim” base image because we need a package manager to add Async Profiler. Also note that when you add the async-profiler package from the Alpaquita repository, the libstdc++ package required by Async Profiler will be added automatically. In the ENTRYPOINT, we attach the profiler as a Java agent and provide some options such as starting the profiler upon application startup, the event we want to profile, and the name of the file with the profiling data. Other available options are described in the docs. It is also possible to perform interactive profiling in the container. I’ll show you how to do that below. After building the image, start the container. As Docker containers restrict the access to perf_event_open syscall, you can use one or several of the following approaches: Add the --cap-add SYS_ADMIN option to access privileged perf event information; Disable the default Docker seccomp profile that disables some system calls with the --security-opt seccomp=unconfined option; Use --fdtransfer together with running the container as privileged user to allow using perf_events; Fall back to -e ctimer profiling mode, which is similar to cpu mode, but does not require perf_events support. In addition, you need to configure the Linux kernel to allow access to the performance events system. For that purpose, set the perf_event_paranoid to 1 and kernel.kptr_restrict to 0: sudo sysctl kernel.perf_event_paranoid=1 sudo sysctl kernel.kptr_restrict=0 Note that these options are available for Linux only, so if you use macOS, the profiling information will be limited. The command for running the container may look like this: docker run -p 8082:8080 --name pet --cap-add SYS_ADMIN --security-opt seccomp=unconfined petclinic-async Profiling started Now, how do we get the profiling data? The profile.html file will be created automatically in the /tmp subdirectory of the container when you stop it, but what if we don’t want to stop the container running in production? In this case, you can sh into the running container with: docker exec -it pet sh We are inside the container. As we have installed the async-profiler package, the asprof tool is at our command. It can be used to stop the profiling session, resume it, or dump data without stopping the profiling. Let’s find out the PID of the process, which we need to pass to the asprof tool. Then, use asprof to dump the data into the file: /app # ps PID USER TIME COMMAND 1 root 0:14 java -agentpath:/opt/async-profiler/lib/libasyncProfiler.so=start,event=cpu,file=/tmp/profile.html -jar -XX:MaxRAMPercentage=80.0 /app/petclinic.jar 47 root 0:00 sh 64 root 0:00 ps /app # asprof dump -f /tmp/profile.html 1 After that, copy the file to the current directory by running: docker cp :/tmp/profile.html . This approach can be used for interactive profiling as well. Simply sh into the running container and use asprof to start the profiling session. It can be as simple as running /app # asprof start 1 Alternatively, use the options specified in the documentation to adjust the profiling as you see fit. Profile Java containers with Async Profiler from host If you have full access to the host server, profiling from the host can be very convenient. First of all, you can tap into a running container at any point in time without having to run a profiler in the background continuously. Secondly, you don’t have to fiddle with additional settings in the image or Docker. To profile a Docker container from the host, you need to run Async Profiler as a privileged user. Also, the container must be able to access the profiler by the same absolute path as on the host. To do that, start the container with the following command: docker run -v /absolute/path/to/async-profiler-3.0-linux-x64:/absolute/path/to/async-profiler-3.0-linux-x64 -d -p 8082:8080 --name pet --cap-add SYS_ADMIN petclinic Now, let’s find out the PID of a running container as we need to provide it to the profiler: docker top pet Finally, you can start the profiling session. Instead of PID use the number you got as a result of the previous command: sudo ~/async-profiler-3.0-linux-x64/bin/asprof -e cpu -d 15 -f /tmp/profile.html PID The profile.html file will be saved to the /tmp subdirectory of the container. Use Async Profiler with buildpacks If you build container images using buildpacks, there are a couple of factor you need to consider. Firstly, Async Profiler requires the libstdc++ library to work. But Spring Boot 3.4+ uses a new builder paketobuildpacks/builder-jammy-java-tiny, which doesn't include this library. As a result, if you want to profile the application from the container, you should change the builder to paketobuildpacks/builder-jammy-base: org.springframework.boot spring-boot-maven-plugin paketobuildpacks/builder-jammy-base Secondly, you need to configure the Linux kernel to allow access to the performance events system, or else the profiling won't start. For that purpose, set the perf_event_paranoid to 1 and kernel.kptr_restrict to 0: sudo sysctl kernel.perf_event_paranoid=1 sudo sysctl kernel.kptr_restrict=0 Thirdly, buildpacks create a layered JAR by default. If you want to profile the application with Async Profiler from the start, the profiler must be in the target container. To do that, you need to download the Async Profiler bundle for Linux and place it into the resources directory of your project. In this case, the buildpacks won't discard the profiler files. After including the profile directory into the project, we need to add the JVM option specifying the path to the profiler. In a layered JAR, the files from the resources directory are located in workspace/BOOT-INF/classes. So, specify the path and profiler options options with BPE_APPEND_JAVA_TOOL_OPTIONS. org.springframework.boot spring-boot-maven-plugin paketobuildpacks/builder-jammy-base -agentpath:/workspace/BOOT-INF/classes/profiler/lib/libasyncProfiler.so=start,event=cpu,file=/tmp/profile.html Build the container image. Start the container. You may need to add the --cap-add SYS_ADMIN option and the --security-opt seccomp=unconfined option to access privileged perf event information: docker run -p 8080:8080 --cap-add SYS_ADMIN --security-opt seccomp=unconfined --name pet petclinic The file with the profiling data can be found in the /tmp subdirectory of the container after you stop the container. Due to security restrictions baked into buildpacks, it isn't possible to use asprof in the container to dump profiling data without stopping the container. So, to tackle this issue, you can use another approach with buildpacks, i.e., profiling a containerized application from host. See the section above for instructions. Note that you can remove the async-profiler directory and the additional options from the pom.xml, but you still need to use the paketobuildpacks/builder-jammy-base builder. Best practices of profiling containerized workloads There are several tips on taking the most out of a profiling session without major impact on application performance in production: 1. To minimize the impact of profiling, use short profiling intervals and avoid running the profiler continuously in production. 2. Define the key performance indicators (KPIs) to understand what deserves optimizations based on the aquired metric data. 3. Always test profiling in a staging environment before using it in production. 4. Use profiling in the testing environment to assess the impact of profiling on application performance and identify performance issues at an early stage. Conclusion In this article, we looked into approaches to profiling containerized Java applications with Async Profiler, a low-overhead open source profiler. You can also profile Java apps with Java Flight Recorder, a profiling and diagnostics tool built into the OpenJDK. Refer to my previous guide for more details. - [Oracle Java 8 and 11 End of Life: Risks, Migration Strategies, and Best Alternatives](https://bell-sw.com/blog/oracle-java-8-and-11-end-of-life-risks-migration-strategies-and-best-alternatives/): Oracle Java 8 and 11 will reach their End of Life in several years, meaning that there will be no updates— free or paid. Running mission-critical workloads on unsupported Java versions introduces significant enterprise risks, from increasing operational costs to potential compliance violations. This guide examines the implications of what Java End of Life means for business operations and outlines cost-effective migration strategies to ensure continuous security and performance. Table of Contents What Does End of Lifecycle Mean? Java's Evolving Release Cadence Business Risks of Unsupported Java Versions Java 8 and 11 End of Life: The Final Countdown Migration Strategy: How to Move from Java 8 or 11 to a Newer Version Increased Performance and Extended Life of JDK 8/11 with Liberica JDK Performance Edition Conclusion: Proactive Planning Prevents Business Disruptions FAQ What Does End of Lifecycle Mean? End of Life (EOL) marks the definitive point when a software version receives its final update and all vendor support permanently ceases. For Java deployments, EOL creates an immediate technical inflection point with far-reaching business implications: Security patches stop completely, even for critical vulnerabilities; Bug fixes and performance improvements terminate permanently; Technical support options disappear entirely, regardless of willingness to pay. Java's Evolving Release Cadence Java's release model transformed from an unpredictable schedule to a structured cadence starting with Java 9. The current model introduced with Java 17 delivers LTS versions every two years (Java 17, 21, 25) with regular six-month feature releases between them. Security fixes arrive through quarterly Critical Patch Updates, which are available for all currently supported LTS versions (8, 11, 17, 21) and the current feature version. When a Java version crosses its EOL threshold, applications immediately begin accumulating technical debt and security vulnerabilities that cannot be remediated through normal update channels. This creates a rapidly expanding risk surface that impacts security posture, operational stability, and regulatory compliance. Business Risks of Unsupported Java Versions When Java 8 and 11 reach end-of-life status, businesses face a critical inflection point—not simply an IT maintenance milestone. The end of vendor support creates a cascade of technical vulnerabilities and operational risks that directly impact business continuity: Security Vulnerabilities & Data Breach Threats Every quarter, Oracle's Critical Patch Updates address dozens of newly discovered vulnerabilities. Once EOL occurs, these vulnerabilities remain permanently unpatched in your production environment. Financial institutions and enterprises running business-critical applications face the added problem of failing security audits and insurance coverage reviews, potentially invalidating cyber insurance policies precisely when they're most needed. Operational Failures & System Instability The production environment surrounding Java doesn't remain static. Database vendors, application server providers, and library maintainers all move forward with their technology roadmaps—often dropping support for legacy JDKs without explicit warnings. When these failures occur, recovery isn't simple. Emergency migrations under pressure typically require: 3-6 months for straightforward applications; 6-12+ months for complex enterprise systems; Significant unplanned engineering resources diverted from strategic initiatives. During this emergency migration period, production systems often remain partially impaired or running on improvised workarounds that introduce additional stability risks. Regulatory Non-Compliance & Legal Exposure Modern regulatory frameworks explicitly require actively maintained software components Operating EOL Java versions creates direct compliance violations across multiple frameworks: The Cyber Resilience Act (CRA) requires that software suppliers "ensure that vulnerabilities can be addressed" throughout the product's lifecycle—impossible with EOL Java. The PCI Security Standards Council (PCI SSC), an international information security standard used to handle credit cards, requires organizations to "protect system components and software from known vulnerabilities" in section 6.3.1, with auditors automatically flagging EOL runtime components. The Health Insurance Portability and Accountability Act of 1996 (HIPAA) requires implementing "technical security measures to guard against unauthorized access"—unpatched JVMs fail this requirement by definition. Federal Information Security Modernization Act (FISMA) requires timely remediation of vulnerabilities, with EOL software creating permanent unresolvable compliance issues. Failed compliance audits trigger a cascade of consequences, including financial losses, reputational damage, and regulatory penalties. Increased Infrastructure Costs While modern JVMs deliver significant performance improvements through better garbage collection, memory management, and JIT compilation, legacy JVMs lack these optimizations—creating a growing efficiency gap. This efficiency gap translates directly to infrastructure costs. Organizations running legacy JVMs typically require: 20-40% more compute resources for equivalent workloads; 30-50% higher peak capacity provisioning; 2-3x more frequent scaling events under variable load. This makes cloud cost optimization nearly impossible as you're forced to compensate for fundamental runtime inefficiencies by over-provisioning resources. Java 8 and 11 End of Life: The Final Countdown Java 8 and 11 will reach their end-of-support milestones on different schedules depending on your chosen runtime provider. These deadlines represent hard cutoffs after which no security patches or fixes will be available—even for critical vulnerabilities. Let’s look at the major JDK distributions. Oracle JDK Oracle doesn’t provide free updates for Oracle Java 8 and 11. The updates are available only as part of commercial support. Oracle Java 8 reaches End of Life in December 2030, while Oracle Java 11 meets EOL in September 2026. The next LTS release of Oracle Java 17 follows a new licensing model, according to which free updates are provided for three years. After that, the enterprise must migrate to the next Oracle Java LTS version or acquire a subscription. Commercial support for Oracle Java LTS 17+ is provided for eight years. Oracle Java 17 reaches EOL in September 2029. OpenJDK Vendor Alternatives Alternative OpenJDK vendors provide free quarterly updates for JDK 8 and 11 and commercial support until the End of Life of their respective distributions. LTS versions 17+ also receive free updates for five and more years depending on the vendor. For example, BellSoft offers the longest support period for JDK 8 and 11. BellSoft’s OpenJDK distribution Liberica JDK 8 will reach End of Life in March 2031, and Liberica JDK 11 meets EOL in March 2032. JDK Version Oracle JDK(free updates end) Oracle Java SE (paid EOL) Liberica JDK EOL JDK 8 January 2019 December 2030 March 2031 JDK 11 March 2019 September 2026 March 2032 JDK 17 September 2024 September 2029 March 2030 JDK 21 September 2026 September 2031 March 2032 To summarize, if you would like to stay on Java 8 and 11 as long as possible, you can migrate to Liberica JDK and receive one more year for JDK 8 or six more years for JDK 11. Liberica JDK is a TCK-verified Java runtime, which means that if an application runs on Oracle Java, it will run on Liberica JDK. Migration requires little to no adjustments. Migration Strategy: How to Move from Java 8 or 11 to a Newer Version Enterprise Java migrations require thorough planning to minimize disruption. Legacy applications don't simply run Java—they depend on ecosystems of libraries, frameworks, and tools that must evolve together. Migrating from Java 8/11 to modern versions introduces several challenges: Third-party dependencies requiring updates or replacements; Platform changes necessitating code refactoring; Modified deployment and operational procedures. A successful migration balances technical requirements with business continuity. Starting well before EOL deadlines provides the necessary runway to address compatibility issues methodically rather than under crisis. With these implications in mind, below are the steps that will help you draft a robust migration plan: Assess risks and business impact. Identify applications running on Java 8/11 and prioritize critical systems. More critical services should be migrated first. Consider vendor options. Decide whether you want to extend the subscription with your current vendor or consider alternative OpenJDK offerings. Choose the Right Migration Path: Upgrade to Java 17 or 21 if you have required time and developer resources now. Use an OpenJDK vendor for extended support to get years of JDK 8/11 support if the migration duration is estimated to extend beyond Oracle Java 8/11 EOL . Ensure Compatibility. The team must test all applications with the new Java version before deployment to avoid failures and errors in production. Mitigate Downtime Risks. The team should use a phased rollout strategy to prevent operational disruptions. Plan for Continuous Java Upgrades. Set up a long-term Java upgrade policy to avoid future EOL crises. A complete migration to a newer Java version may take a year or more depending on the complexity of the program and available resources. You can use the guides Migration from JDK 8 to 17 or Migration from JDK 11 to 17 to identify features and options removed in newer versions and ensure smooth transition. While developers solve incompatibility issues and refactor code, the application may experience serious performance degradation. To preserve stable operation and keep up with the modern performance requirements whilst your application undergoes JDK upgrade, consider using a fused JDK Liberica JDK Performance Edition. Increased Performance and Extended Life of JDK 8/11 with Liberica JDK Performance Edition If your migration to Java 17 or 21 has a lengthy timeline, but your business faces growing performance requirements and resource use pressure, Liberica JDK Performance Edition offers a solution. This technology fuses JDK 8 or 11 with the high-performing JVM 17. How does it work? The Performance Edition delivers modern JVM optimizations to legacy applications without requiring code changes: Your application continues using familiar JDK 8/11 APIs and libraries; The underlying JVM leverages Java 17's advanced garbage collection and memory management; Applications gain immediate performance benefits with zero code refactoring. Performance benchmarks demonstrate 10–15% performance improvements —even with the default settings and no tuning. What does it mean for business? Immediate Performance Gains: Enhance application responsiveness and throughput without waiting for complete migration. Reduced Infrastructure Costs: Lower resource utilization through efficient garbage collection and memory management. Zero Migration Risk: Maintain full compatibility with existing application code and dependencies. Extended Transition Timeline: Gain breathing room for methodical migration planning while immediately addressing performance needs. No Vendor Lock-In: Built on 100% open source, TCK-verified components for seamless transitions Experience these benefits firsthand by requesting Liberica JDK Performance Edition demo builds for testing with your application. Evaluate performance improvements in your specific environment before making deployment decisions. Request Liberica JDK Performance Edition for Testing Conclusion: Proactive Planning Prevents Business Disruptions The impending End of Life for Java 8 and 11 is not merely a technical milestone—it represents a critical business risk inflection point. When these platforms stop receiving security updates and fixes, organizations face a cascade of potentially severe consequences: Security vulnerabilities that remain permanently unpatched, creating expanding attack surfaces; Regulatory non-compliance leading to failed audits, legal penalties, and contractual breaches; Sudden application failures as the surrounding technology ecosystem evolves; Spiraling infrastructure costs from inefficient runtime performance; Emergency migration projects with compressed timelines and elevated failure risks. Organizations that wait until EOL deadlines approach find themselves with severely limited options. The time to act is now, while multiple strategic pathways remain available: Conduct a comprehensive Java inventory to identify all affected applications and dependencies; Develop a prioritized migration roadmap based on business criticality and technical complexity; Leverage performance optimization solutions like Liberica JDK Performance Edition to enhance application efficiency while planning longer-term migrations; Establish a sustainable versioning strategy that aligns with vendor support timelines to prevent future EOL challenges. By taking proactive steps, companies can protect their operational stability while achieving improved performance, reduced costs, and enhanced security. FAQ What if my application isn’t compatible with Java 17 or 21? The application may use features or JVM options deprecated in newer Java versions. In this case, you have to refactor the code to be able to use JDK 17 or 21. How long does Java migration take? The migration period depends on the complexity of the application and is different for each case. The more dependencies and legacy Java features the project uses, the longer will the migration take. What is the exact end-of-life (EOL) date for Oracle Java 8 and 11? Oracle Java 8 reaches End of Life in December 2030, Oracle Java 11 reaches EOL in September 2026. What happens if I continue using Java 8 or 11 after EOL? Staying on Java versions past their EOL may lead to audit failures, operational failures, and cybersecurity threats. How do I check if my applications depend on Java 8 or 11? The fastest way to check the JDK version is to run java -version. Can I upgrade directly from Java 8 to Java 17 or 21? Yes, you can, but it will take time to solve all incompatibilities and update library versions. What is the safest migration path from Java 8/11? Identify the Java versions your project uses; Classify the services according to their significance; Migrate the most critical services to Liberica JDK Performance Edition to increase their performance; Upgrade the services one by one, compile the application and solve issues and errors if any; Run the services to verify their behavior before deploying them in production; Use a phased rollout strategy. Is there a way to delay migration while still improving security and performance? You can migrate to Liberica JDK Performance Edition that couples JDK 8 or 11 and JVM 17. It means that you stay on JDK 8 or 11, but your application receives the performance boost thanks to a newer JVM. In addition, Liberica JDK Performance Edition is supported longer than other distributions, so you will have more time to perform the complete migration. What are the licensing differences between Oracle Java and OpenJDK distributions? Starting with Java 17, Oracle Java is distributed under the Oracle No-Fee Terms and Conditions license. OpenJDK distributions are open source and licensed under GPLv2 + Classpath exception. - [Java Performance Testing: Best Practices & Tools](https://bell-sw.com/blog/java-performance-testing-best-practices-tools/): With the growing adoption of DevOps and CI/CD practices, performance testing has become a shared responsibility between developers, SREs, and performance engineers. If you are new to performance testing, this article is for you! It covers key aspects of performance testing cloud-native Java applications, including critical performance indicators, testing methodologies, and popular open-source tools. Table of Contents What Can Affect Java Application Performance Key Performance Indicators (KPIs) Methods of Performance Testing Profiling & Bottleneck Analysis Load Testing Stress Testing Scalability Testing Spike testing Endurance testing Chaos Engineering Open-Source Tools for Java Performance Testing, Monitoring, and Profiling Best Practices for Java Performance Testing in the Cloud Automate Performance Tests in CI/CD Pipeline and use Canary Releases Design Performance Tests with Serverless and Kubernetes in Mind Use Real-World Traffic Simulation Use Distributed Tracing to Analyze Request Flows Conclusion FAQ What Can Affect Java Application Performance Java application performance refers to the efficiency of the Java application, i.e., how fast it performs the tasks and how many CPU and memory resources it consumes.It is important to understand which factors can affect it so that when you establish key performance indicators and accumulate required metrics, you know where to look for the culprit. There are multiple aspects that can impact Java application performance in the cloud, from application code to cloud infrastructure limits. They can be briefly summarized as follows: Garbage Collection Java boasts automatic garbage collection that relieves the developers of the manual memory management headache. But automatic doesn’t mean carved in stone: Java offers seven garbage collectors, each fit for a specific purpose. In addition, each of the collectors can be configured to reach the best result. Small applications can benefit from the default garbage collection settings, but in the case of enterprise projects, default or improperly configured GC can lead to performance deterioration. Frequent GC pauses and improper heap usage can lead to increased latency, decreased throughput, excessive memory and CPU overhead, or even application crashes. Thread Contention & Synchronization Multithreaded programming can make the program highly efficient, but it is fraught with potential caveats such as thread contention, thread locks, and deadlocks, which may result in application slowing down or even hanging. JVM Configuration JVM configuration includes adjusting various parameters affecting application performance, including heap size, RAM consumption, GC implementation, GC tuning, compilation level, and many others. For instance, improper heap sizing may result in application crashing with the OutOfMemoryError, and implications of improperly set garbage collector were discussed above. Cloud Infrastructure Limits Numerous factors may impact the efficiency of the cloud infrastructure and the cloud costs. A cloud infrastructure built with out-of-the-box technologies and default Kubernetes settings may limit the application performance under peak loads or in the case of traffic increase. The containers may start consuming too many resources, leading to CPU throttling, when Kubernetes limits the amount of resources available to containers, slowing down the application. In addition, the application may behave differently in the cloud than on the bare metal, leading to inadequate memory consumption, excessive auto-scaling, and pods shutting down unexpectedly. Container configuration Some of the common containerization mistakes include improper JVM resource limits in Docker and Kubernetes, inadequate CPU and memory constraints, or lack of container-aware GC tuning. As a result of using improper or default container settings, the application may over- or underutilize available resources, slow down, or even exit unexpectedly. Key Performance Indicators (KPIs) Key Performance Indicators or KPIs are quantifiable measures used to assess the application performance. Before starting any performance testing, it is essential to understand what is expected of your application. This can be done by defining Service Level Agreements (SLAs), which are agreements between a service provider and users or clients. SLAs can be seen as performance targets, whereas KPIs are the metrics that show the actual performance of the application in terms of meeting these targets. In a perfect world, an application works super fast, consumes minimal resources, and can manage the heaviest loads. Unfortunately, we can’t have it all as these performance indicators often compete with each other Therefore, KPIs should be defined individually based on business requirements. The most important KPIs for Java applications include: Response time or latency: Time taken to process a request. Throughput: How many user requests the application can handle in a given timeframe. Throughput is usually measured as Requests per Second or RPS. Startup time: Time the application needs to start processing incoming requests. Enterprise Java applications usually take several seconds to start. But this KPI should also include the warmup time, which is the time taken by the app to reach peak performance. During the warmup, the application processes fewer requests, but consumes more resources. Slow Java application startup and warmup can be a problem in the cloud. CPU & Memory Usage: The amount of CPU and memory consumed by the Java application. Error Rate & Fault Tolerance: The ability of the application to maintain stable work despite errors or faults in its components. Methods of Performance Testing There are several types of performance tests. Each of them serves a different purpose. Some identify performance bottlenecks, while others assess system resilience, scalability, or reliability. Below are the key performance testing methods that you should consider when evaluating application performance. Some of the tools mentioned in this section are summarized in more detail further below. Profiling & Bottleneck Analysis Profiling is a method of assessing application behavior by gathering metrics on memory allocation, thread behavior, garbage collection, and other JVM operations. Java profiling can help you identify performance bottlenecks, including code hotshots, thread synchronization problems, and even memory leaks. You may also need a profiler to gather metrics during the performance tests. There are numerous tools for Java profiling, from easy-to-use profilers that gather essential JVM metrics to ~Application Performance Monitoring (APM) solutions that offer multiple features including infrastructure monitoring or inter-services communication evaluation. You can read a detailed guide to Java profiling to find out about profiling tools, techniques, and best practices. Two great open-source tools for Java profiling are Java Flight Recorder built into the JVM and a lightweight GUI-less Async Profiler. Load Testing Load testing evaluates how the application behaves under normal and increased workloads. It simulates the interaction of a varying number of concurrent users with the system to determine response times, throughput, and memory utilization. Load testing helps to ensure that the application meets the SLAs under various traffic conditions. Some tools for Java load testing are Apache JMeter, Gatling, and BlazeMeter. Stress Testing Stress testing is aimed at pushing the system beyond limits to identify breaking points and recovery tempo. The testing consists in increasing the number of concurrent users interacting with the application until the system fails. As a result, you can define the maximum load the application can handle, identify whether the system gracefully degrades or crashes, and see how fast it resumes normal operation after a failure. Some popular tools for stress testing are Locust, wrk, and Siege. Scalability Testing Scalability testing evaluates how well the application scales horizontally (adding more instances) or vertically (adding CPU/memory) under increased concurrent load. Scalability tests help you identify performance impact of scaling, how much more load the application can handle after scaling, and whether the auto-scaling strategies lead to delayed pod provisioning. If you use Kubernetes, you can take advantage of Kubernetes-specific tools for scalability testing such as K6 and Kubemark. Spike testing Spike testing helps you determine how the application behaves under sudden and extreme traffic surges. Unlike load testing where traffic increase is introduced gradually, these tests subject the application to sharp spikes to see how well the application adapts to them. Some performance issues you may reveal are slow response times, timeouts, or inadequate scaling policies. Spike testing is a necessity for applications that experience unpredictable traffic splashes, such as e-commerce apps during Black Friday or other sales events. You can configure Gatling and k6 to generate sharp traffic spikes. Endurance testing Endurance or soak testing evaluates application behavior over an extended period of time under a stable load. Endurance tests can help you identify memory leaks, resource exhaustion, or gradual performance degradation. You can use JMeter configured for long-duration tests or Locust that supports continuous load generation. Chaos Engineering Chaos engineering can help you assess the resilience of your application, i.e., its ability to function in the case of real-world disruptions and system failures. It is a testing approach when controlled failures such as server crashes or network failures are introduced into the system to evaluate its behavior under stress. Some tools for performing chaos experiments are Chaos Monkey, Gremlin, and Chaos Mesh. Chaos tests help teams to identify single points of failures and improve fault-tolerance of their system. Open-Source Tools for Java Performance Testing, Monitoring, and Profiling There’s no tool that can help you manage all tasks related to Java performance testing, which means that you have to equip yourself with several solutions for comprehensive performance evaluation. Many tools are free, some of them require a subscription or offer freemium licenses. Below is an overview of some of the most widely used open-source tools, categorized by their purpose: Performance testing tools that simulate load, analyze scalability, and measure system responsiveness or resilience; Profiling tools that help to monitor application behavior under the load and identify related CPU, memory, and thread bottlenecks; Monitoring and tracing tools that provide insights into distributed systems and application observability. Note that you should use a combination of tools: a load testing tool for creating a load and a profiler for analyzing application behavior under this load. Tool Category Purpose Features Ease of use JMeter Performance testing Load testing, Performance benchmarking, Endurance testing GUI-based Detailed reporting Moderate: needs time to get familiar with tool’s UI k6 Performance testing Load testing for APIs and microservices Lightweight Clod-native Integrates into CI/CD Easy: simple scripts, but requires JavaScript knowledge Gatling Performance testing Load testing, Stress testing, Spike testing Real-time performance metrics DSL for expressive test scenarios Integrates into CI/CD Easy: simple setup, can write scripts in Java wrk Performance testing HTTP benchmarking Multi-threaded request generation Low overhead Easy: CLI-based, but lacks built-in reporting Chaos Monkey Performance testing Chaos engineering Randomly terminates services or infrastructure to test failure handling Developed for testing in the cloud environment Moderate: requires Kubernetes or cloud infrastructure for testing Locust Performance testing Load testing, Stress testing Can simulate millions of users with distributed execution Web-based UI for real-time test control and reporting Easy: requires scripting, but readable and flexible Java Flight Recorder (JFR) Profiling Low-overhead profiling and performance analysis Integrated into JVMSupports hundreds of profiling eventsSupports custom events Moderate: No setup needed, but requires time to understand the workflow Async Profiler Profiling CPU & allocation profiling with flame graphs Lightweight Can profile native calls Generates flame graphs Easy: simple setup, clear CLI commands Jaeger Monitoring and tracing Distributed tracing for microservices performance analysis Tracks request flows across services Latency analysis in microservices OpenTelemetry integration Moderate: requires integration OpenTelemetry Monitoring and tracing Telemetry data generation Distributed tracing, metric collection, and logging End-to-end observability for cloud-native applications Moderate: requires manual instrumentation Best Practices for Java Performance Testing in the Cloud Java performance testing is crucial for meeting the SLAs and gaining competitive advantage. As modern applications are adapted to the cloud environment or are cloud native by design, performance tests must adapt to these realities. Below are the best practices for testing Java performance for modern demands. Automate Performance Tests in CI/CD Pipeline and use Canary Releases Integrating performance tests into the CI/CD pipeline will help you pinpoint performance bottlenecks and regressions as soon as possible. This way, you can remediate the situation before it hits end users. In addition, use canary releases by rolling out product changes to a small portion of users. This way, you will be able to monitor performance metrics in a controlled manner and roll back quickly in case of issues. Design Performance Tests with Serverless and Kubernetes in Mind Serverless architecture and container orchestration systems such as Kubernetes introduce their own performance challenges that you should consider. For example, the inheritance part of serverless computing where resources are allocated on demand is the cold start issue. Cold starts happen when the cloud service such as AWS Lambda invokes a function for the first time and needs time to initialize it. Cold starts may become a significant problem with Java due to the prolonged startup and warmup times. Therefore, Java startup time assessment and reduction is a must if you want to use FaaS solutions. As for Kubernetes, improperly configured auto-scaling strategies can lead to significant — up to several minutes — delays in pod provisioning. As a result, you need to perform load testing to evaluate how Kubernetes Horizontal Pod Autoscaler (HPA) and Vertical Pod Autoscaler (VPA) behave under the sudden traffic spike. Use Real-World Traffic Simulation Using synthetic workloads or testing the cloud-native application on bare metal isn’t very useful. You should simulate the real-world scenarios, traffic patterns, and the production environment to gain realistic insights into the application performance. In addition, you can integrate user behavior variations to gain even more accurate testing results. Simulating real-world production environments, scenarios, and traffic surges will help you ensure that performance optimizations are aligned with the actual user interactions and expectations. Use Distributed Tracing to Analyze Request Flows In the microservices architecture, performance bottlenecks are often difficult to pinpoint because requests often pass through multiple services. Distributed tracing tools like OpenTelemetry and Jaeger help track how requests move across services. This will help you identify latency sources. By visualizing service-to-service interactions, database queries, and external API calls, developers can pinpoint slow components and cascading failures. As a result, it will be easier to optimize critical execution paths and avoid issues in the case of dynamic scaling. Conclusion Java performance testing is an essential procedure that helps IT teams meet SLAs and ensure that applications can handle various situations and traffic spikes without crashing or eating away at resources. The sooner you integrate performance testing in your workflows, the less performance issues you have to solve in production. So, what should be your next steps? Define KPIs; Choose the testing tools and set up the tests; Set up continuous performance monitoring in production; Optimize the performance if test results are unsatisfactory. You can check out the Guide to optimize Java performance in Kubernetes and the Overview of Java performance optimization techniques to define a suitable performance optimization strategy. FAQ How can developers integrate performance testing into CI/CD pipelines? Performance tests can be integrated into CI/CD pipelines by using tools like JMeter, k6, or Gatling. Tests can be triggered on each deployment, with threshold-based pass/fail criteria. What are the key performance indicators (KPIs) for Java applications? Key KPIs include response time, throughput (RPS), CPU and memory usage, startup time, and error rates. Which open-source tools are best for Java performance testing? The best open-source tools are Gatling, k6, and Chaos Monkey for performance testing, Async Profiler and JFR for profiling, Jaeger and OpenTelemetry for distributed tracing and observability. How can I simulate real-world user behavior in Java performance testing? You can use JMeter, Gatling, or Locust to create test scenarios that mimic real-world interactions, including randomized think times, variable request patterns, and concurrent users. When should performance testing be conducted in the software development lifecycle? Performance testing should be done continuously: During development using unit-level benchmarks; In CI/CD pipelines for regression testing; Before production releases under realistic load conditions; In production monitoring to detect real-world performance issues. How do I measure the impact of third-party APIs and external services on application performance? You can use distributed tracing tools like OpenTelemetry and Jaeger to track API call latencies. Load testing with JMeter or k6 can simulate third-party API failures to analyze their impact on your system. - [Liberica JDK 24 is Released](https://bell-sw.com/blog/liberica-jdk-24-is-released/): Liberica JDK 24 is Released We are happy to announce the release of Liberica JDK 24! This is a final feature release before the next LTS release due in September 2025, so if you are planning the migration to JDK 25, JDK 24 gives a great opportunity to test new features and prepare for the upgrade. Liberica JDK 24 release contains 2,597 fixes in JDK and 175 fixes in FX, 24 Java Enhancement Proposals (JEPs) with new, enhanced, or deprecated functionality. Download Liberica JDK 24 Summary of JEPs in JDK 24 Finalized Features JEP 484: Class-File API provides a standard API for working with Java class files. JEP 485: Stream Gatherers introduces custom intermediate operations to Stream API. New Features Increased performance JEP 404: Generational Shenandoah (Experimental) enables Shenandoah GC to separate the Java heap into young and old generations to collect young objects more frequently, thus reducing the CPU usage and memory footprint. JEP 475: Late Barrier Expansion for G1 shifts G1 GC barrier extension to later phases of JIT compilation, reducing the CPU and memory usage during the JVM warmup. JEP 483: Ahead-of-Time Class Loading & Linking creates the AOT class with initialized and linked application and system classes to reduce the JVM startup time. Decreased Memory Footprint JEP 450: Compact Object Headers (Experimental) reduces the size of Java object headers to 64 bits on 64-bit architectures, thus reducing the Java heap size. JEP 493: Linking Run-Time Images without JMODs enables the jlink tool to exclude the JMOD files from custom runtime images, reducing their size by 25%. Hardened security JEP 478: Key Derivation Function API (Preview) introduces cryptographic algorithms for deriving additional keys from a secret key and other data. JEP 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism introduces modern cryptography algorithms designed to be secure against quantum computing attacks. JEP 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm introduces a modern digital signature algorithm designed to be secure against quantum computing attacks. Enhanced Features Facilitated development JEP 487: Scoped Values (Fourth Preview) introduces one change to the Scoped Values API, which is the removal of the callWhere and runWhere methods. JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) re-previews the feature without changes. JEP 492: Flexible Constructor Bodies (Third Preview) re-previews the feature without significant changes. JEP 494: Module Import Declarations (Second Preview) introduces two additions to the feature. These are the ability to use type-import-on-demand declarations and the removal of the restriction that prohibited modules to declare a transitive dependence on the java.base module. JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview) rep-previews the feature without changes, except for a revised title and terminology. JEP 499: Structured Concurrency (Fourth Preview) re-previews the feature without changes. Increased Performance JEP 489: Vector API (Ninth Incubator) re-previews the feature with several important changes, including the introduction of a value-based class Float16 and changes to the selectFrom cross-lane operation, the selectFrom and rearrange cross-lane operations, transcendental and trigonometric lanewise operations JEP 491: Synchronize Virtual Threads without Pinning allows virtual threads to mount and unmount platform threads in the synchronized method, eliminating negative impacts on scalability. Removed and Deprecated Features JEP 479: Remove the Windows 32-bit x86 Port removes the source code and build support for the Windows 32-bit x86 port as the last Windows OS supporting 32-bit operation will reach EOL in October 2025. JEP 486: Permanently Disable the Security Manager disables the deprecated Security Manager API, which will be removed completely in the future releases. JEP 490: ZGC: Remove the Non-Generational Mode removes the non-generational mode of the low-latency ZGC because this collector became generational by default in JDK 23. JEP 501: Deprecate the 32-bit x86 Port for Removal deprecates the 32-bit Linux x86 port, the only 32-bit x86 port remaining in the OpenJDK, which will be removed in future releases. Preparation for Restriction or Removal JEP 472: Prepare to Restrict the Use of JNI makes the JVM issue warnings upon the usage of Java Native Interface and the Foreign Function and Memory API. In the future, the developer will have to explicitly enable native access, or else the JVM will throw exceptions. JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe makes the JVM issue warnings upon the usage of deprecated memory-access methods in the sun.misc.Unsafe. These methods will be removed completely in future releases. You can read more about each JEP in our Overview of JDK 24 Features or watch a dedicated video overview. Download Liberica JDK 24 builds now! Regardless of whether you are planning to migrate to the next LTS release or just want to play around with new Java features, you can download and use Liberica JDK for free. Head over to the Liberica JDK Download Center to get the fresh JDK builds for your platform. Download Liberica JDK 24 - [How to Contribute to OpenJDK](https://bell-sw.com/blog/how-to-contribute-to-openjdk/): Millions of developers worldwide use Java. The Java platform powers thousands of applications, from small hobby projects to large mission-critical enterprise systems. Java owes its success to numerous organizations and committers that drive the platform forward. But did you know that you can contribute to your favorite programming language, too? Contributing to OpenJDK isn’t only for language designers. Whether you want to fix a small bug, improve performance, enhance documentation, or even propose new features, there’s a place for you in the OpenJDK community. In this guide, we will show you how OpenJDK development works under the hood and how you can contribute. We will also describe steps the proposal must go through before it becomes part of Java SE, using BellSoft’s recent API contribution to the OpenJDK as an example. If you’ve ever wanted to leave your mark on Java, this is your chance! Table of Contents Key Areas of OpenJDK Contributions Criteria for Contribution Acceptance How to Get Started The Contribution Workflow Towards a New Method in Java Conclusion Key Areas of OpenJDK Contributions OpenJDK is a centralized place that unites work on the open-source implementation of Java Standard Edition and the related projects. OpenJDK is an extensive project, whose maintenance and development necessitate a great deal of effort from hundreds of developers. As such, all kinds of help are appreciated. So, if you are not ready to propose a brand-new Java feature, there’re plenty of other ways you can contribute to Java: Test the latest OpenJDK builds to find bugs The larger the codebase, the higher the risk of bugs introduced by changes. The OpenJDK project contains more than 12 million lines of code, and it is challenging to make sure that no fix or enhancement introduces a small bug somewhere in the farthest corner of the project. Therefore, you can download the current OpenJDK build and set off to a bug hunt. Test the builds on your Java projects and report the bugs if any. This information will help the platform developers to resolve compatibility issues and preserve the reliability of OpenJDK. Propose a fix to the existing bug You can fix a known bug. Browse the JDK Bug System, and if you find an issue that you feel you are capable of solving, start working on the solution. You can investigate the root cause, write a fix, and submit a patch for review. Submitting a bug fix not only improves the experience of Java developers worldwide but also helps you build your reputation in the community. Review the new code Proposed changes to the Java code are evaluated by Reviewers. But even if you are not an official OpenJDK Reviewer, you can provide feedback on code clarity, correctness, or potential performance improvements or regressions. You can also review the tests and find corner cases that lack test coverage. By engaging in reviews, you not only help maintain Java’s high-quality standards but also learn best practices from seasoned OpenJDK contributors. Reviewing code is a great way to get familiar with the development process before submitting your own changes. Improve documentation You can contribute by fixing outdated JavaDoc comments, enhancing API documentation, or adding missing explanations in OpenJDK guides. Even small changes such as clarifying method behavior or improving examples can make a big impact. Offer a new Java Enhancement Proposal (JEP) If you have an idea for a significant improvement to Java, you can propose a JEP (Java Enhancement Proposal). JEPs are used for major changes that may involve changing several parts of JDK and solicit cooperation with several OpenJDK Project teams. Therefore, a JEP must go through a rigorous evaluation process, including feasibility studies and discussions with the Java community. Contributing a JEP is a long-term commitment, but it’s how groundbreaking features like Records, Virtual Threads, and Pattern Matching have become part of Java. Submit code to the existing JEP currently under development Not every contribution has to start from scratch. You can join the work on the active JEP. Besides, OpenJDK projects like Leyden and Valhalla always need developers to write code, optimize performance, or test new features. By contributing to an existing JEP, you get a chance to work with experienced OpenJDK developers and participate in Java’s evolution. Blog about Java’s new features and releases OpenJDK Contributions are not only about code. You can blog about new Java features, showcasing how they helped you with your project. You can also write tutorials as many developers need good guides to understand how to use Java’s capabilities to the maximum. By explaining Java’s recent improvements, providing real-world examples, or breaking down complex topics, you can make Java more accessible to everyone. You can also spread the word about the newest Java releases to help drive the adoption of the newest Java versions forward. Criteria for Contribution Acceptance Not all proposals will be accepted, and this is not because OpenJDK maintainers are cantankerous or haughty. On the contrary, they work hard to preserve the stability of the platform used by millions of people and make sure that the introduced changes do not adversely affect the OpenJDK codebase, now or potentially in the future. Consequently, the maintainers take several factors into consideration when evaluating proposed changes: The stability of the JDK builds must be preserved. Any change, even the smallest one, may introduce bugs or regressions, so the risk/benefit ratio is always carefully assessed. The changes introduced to OpenJDK will remain there for many years ahead, so they must not pose challenges to the future code maintainers. In the complex OpenJDK code, all features interact with each other. As a result, a new feature may cause a negative rippling effect that will be hard to deal with. All changes must adhere to the existing specifications such as the Java Language Specification, the Java Virtual Machine Specification, and the Java API Specification. Therefore, before plunging into coding, make sure that your improvement corresponds to the following criteria: Correctness: The change must be technically sound and not introduce regressions. Evaluated performance impact: Any impact on performance must be evaluated and justified. Compatibility: Changes should not break backward compatibility unless explicitly intended. Tests and documentation: Contributions must include test cases and documentation updates. How to Get Started So how do you get started with Java contributions? First of all, set up your environment. Download the OpenJDK source code, install necessary build tools and compile the code on your machine. Secondly, study the OpenJDK Developer’s Guide. It contains essential information on established development processes, tooling, review policies, and so on. Thirdly, browse the mailing lists. They will help you understand the inner workings of the OpenJDK development and the culture. In addition, Before starting work on the improvement, it is recommended to join or start a discussion on the mailing list. This will help you gain validation on your proposal, sparing you the unnecessary effort if it is deemed as incomplete or unpromising. If you want to start with fixing bugs, find an issue to work on. Browse the JDK Bug System for open issues that range from beginner-friendly small fixes or complex feature enhancements. The Contribution Workflow As mentioned above, any contribution must go through the established development process that includes discussions, reviews, rigorous testing, and approval. Let’s look at one workflow scenario using one of our recent customer cases that evolved into a brand new API in OpenJDK as an example. We’ll need to set up our environment by forking the OpenJDK project. I already have it for the purpose of checking the enhancements and verifying that the OpenJDK tests pass in the GHA workflow. Towards a New Method in Java This particular contribution spawned from a peculiar issue. In the case if a Windows host had a fully numerical name like 000000123456789, this name were treated by the existing Java API InetAddress#getByName()in a specific way. In particular, the resulting InetAddress object sometimes pointed to an nonexistent IP address, although the conversion was made strictly in accordance with the Java specification. The observable behavior was that some of the host names produced valid InetAddress objects pointing to actual IP addresses, some produced invalid objects, and the criteria was unclear. The problem was that the InetAddress API was obsolete and didn’t meet the modern requirements. Namely, the lack of transparency on what’s been passed to the underlying low level OS API (POSIX getaddrinfo in particular), that could lead to blocking calls to Name Resolution system with unpredictable delays while simply parsing a textual IP address and deriving an InetAddress object from it. Also the API didn’t support the textual API address formats defined by POSIX inet_addr API supported by all major browsers and network utilities, that have decimal, octal and hexadecimal address segments. The “healthy” host names turned to be “invalid IP address literals” that went through name resolution, while the others were “valid IP address literals” that have been converted directly to the binary form. Also some of the “invalid” literals in Java were actually valid - in POSIX standard. The first enhancement being developed around that time by Alexei Efimov was JDK-8272215 that introduced a new method InetAddress#ofLiteral() that didn’t cause any blocking calls to the network, and parsed textual addresses in the old style Java dotted-decimal format. The idea was to extend that new API to add the support for POSIX inet_addr-compatible address segments, so as to make Java interoperable with external networking tools and configurations. Initial bug report was filed under JDK-8315767 in September 2023 with the description of the issue. After thorough discussion, the conclusion was to extend the InetAddress API, and the bug was upgraded to ‘Enhancement.’ A Pull Request was proposed with the initial version of the enhancement in March 2024. Upon the subsequent review and discussions it was decided that the Compatibility & Specification Review (CSR) was needed because the improvement necessitated changes to the API. Consequently, a CSR issue was filed the same month. In April 2024, the Pull Request was marked as ‘Ready for Review.’ The further development was on improving and finalizing the new API and related JavaDoc comments as part of the PR. Finally, the CSR was approved and integrated in May 2024. The new method Inet4Address#ofPosixLiteral() was included into JDK 23 released in September 2024. As a result of integrating this API, Java can now do what any modern browser can, namely parsing the textual API addresses using InetAddress#ofPosixLiteral() API according to the POSIX Standard. Conclusion Contributing to OpenJDK means shaping the future of Java. Whatever you decide to do: fix a small bug, improve documentation, or help review code, every contribution makes a difference. The process may seem complex at first, but with persistence and engagement, you’ll gain valuable experience and become part of a community that drives Java forward. So, go ahead and set up your environment, explore the JDK Bug System, and take your first step toward becoming an OpenJDK contributor! - [Liberica Native Image Kit 24.2.0 is released](https://bell-sw.com/blog/liberica-native-image-kit-24-2-0-is-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 24.2.0 for JDK 24. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable Enhancements Added experimental support for the jcmd tool on Linux and macOS that complements the existing Native Image monitoring capabilities such as Java Flight Recorder (JFR); Updated Node.js to 22.13.1; Updated Ruby to 3.3.5. Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. Download the latest version of Liberica NIK now! Download Liberica Native Image Kit - [How to Create JavaFX Native Images](https://bell-sw.com/blog/how-to-create-javafx-native-images/): Combining JavaFX-based applications with GraalVM Native Image will enable you to create platform-specific executables that don’t require JVM to run. In this article, we will look into two ways of turning JavaFX applications into native images: manually and with the Maven plugin. We will also learn to integrate this process into the CI/CD pipeline with GitHub Actions. The code for the project I use in this article is available on GitHub. It was built using Liberica JDK Full that includes an OpenJFX bundle. It facilitates developing and building JavaFX applications as there’s no need to add separate dependencies for JavaFX. The article describes setting up GraalVM with a Maven-based project. For a Gradle-based demo with the native image setup, refer to this code on GitHub. Table of Contents Set Up GraalVM for JavaFX Native Compilation Collect Metadata Using GraalVM Tracing Agent Build a JavaFX Native Image with GraalVM Use GitHub Actions to Automate JavaFX Releases Conclusion Set Up GraalVM for JavaFX Native Compilation IMPORTANT NOTE: you will need to provide the executable JAR of your application to the Native Image, so make sure that you create one before proceeding. Regardless of whether you plan to use a build system plugin or manual compilation, you need to install a GraalVM distribution that supports JavaFX such as Liberica Native Image Kit (NIK). Download Liberica NIK Full with OpenJFX for LTS JDK 17 or 21. You can also use package managers such as SDKMAN! Install Liberica NIK. You can set $JAVA_HOME or create a &NIK_HOME environmental variable to store the path to the installed package. If you want to build native images using Maven, you need to add the plugin to your pom.xml file. It would be more convenient to add it to profiles: native org.graalvm.buildtools native-maven-plugin 0.10.5 true build-native compile-no-fork package com.java.MyApp We’re all set up, let’s move on! Collect Metadata Using GraalVM Tracing Agent Java dynamic features and resources such as images or icons require special treatment when working with GraalVM Native Image. This is because the native-image compiler includes only those elements into the executable that are reachable at build time. Luckily, there are several ways of overcoming this obstacle: If your JavaFX doesn’t use any dynamic features, you can use the -H:IncludeResources='com.fxapp.resources.images.*' flag to make the compiler include resources into the executable. You can specify metadata manually in a JSON file. You can useTracing Agent to collect metadata automatically. I would recommend using Tracing Agent because you cannot be 100% certain that your application doesn’t use or won’t use dynamic features. In addition, if you use FXML files to separate UI code from the business logic, simply adding these files to the native executable won’t help. For Native Image, FXML files are just resources. The compiler doesn’t know that they contain method calls. Let’s run the application with the agent to collect metadata. Enable the Tracing Agent on the command line with the -agentlib:native-image-agent flag, specifying the output directory for JSON files with metadata: $NIK_HOME/bin/java -agentlib:native-image-agent=config-output-dir=./agent-data -jar app.jar The application will start, and you will need to run it through all execution paths so that the agent collects all required data. When the application exits, the JSON files will be automatically generated in the specified directory. You can also use the config-merge-dir option instead of config-output-dir if you need to run the application several times to gather metadata. This option is also useful if you don’t want to collect metadata from scratch every time you update the project. $NIK_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=./agent-data -jar app.jar It is possible to enable the agent in the pom.xml. Follow the instructions described here. Build a JavaFX Native Image with GraalVM Now that we have all necessary metadata on our hands, it’s time to generate a native image. If you do it manually, run the following command with a -H:ConfigurationFileDirectories flag specifying the path to the directory with metadata: $NIK_HOME/bin/native-image -H:ConfigurationFileDirectories=./tracing-agent-data -jar target/lottery-1.0-SNAPSHOT.jar After the compilation has finished, you will find the native executable in the /target directory. You can run it with ./target/myApp If you use the plugin, add the block to the plugin configuration and specify the -H:ConfigurationFileDirectories flag with the path to metadata: myApp target/native com.java.MyApp -H:ConfigurationFileDirectories=./agent-data By default, Maven places the native executable into the /target directory. But you can specify another directory under . After that, run ./mvn -Pnative package The native executable will be created in the /target/native directory. You can now run your executable with: ./target/native/myApp Note that the native executable doesn’t need JVM to run because it already contains all necessary Java classes. Use GitHub Actions to Automate JavaFX Releases In contrast to the JAR files that can be run on any platform where Java is installed, native images are built for a specific architecture. So, if you have built a native image on Linux x66, it will run on Linux x64 only. But what if we want to build an image for another platform? Or what if we release the application for several platforms? In this case, we can use GitHub Actions to build and release the images right from the GitHub. When writing a workflow file for building JavaFX native images, there are several things to consider: You need to specify all operation systems and architectures, for which you want to build native images; Building native images for Linux requires installing additional packages: libasound2-dev, libavcodec-dev, libavformat-dev, libavutil-dev, libgl-dev, libgtk-3-dev, libpango1.0-dev, libxtst-dev; To build native images, you need to use a GraalVM distribution that supports JavaFX. Let’s look at the workflow file that takes all these factors into consideration (you can find this file under native-image.yml in the repository): name: Native Image on: workflow_dispatch: push: paths-ignore: - 'docs/**' - '**/*.md' branches: - main jobs: build_non_win_images: name: 'Build Native Image ${{ matrix.platform }}' needs: [ test_linux_headless ] strategy: matrix: os: [ macos-latest, windows-latest, ubuntu-latest ] include: - os: 'ubuntu-latest' platform: 'linux-amd64' - os: 'macos-latest' platform: 'darwin-arm64' - os: 'macos-13' platform: 'darwin-amd64' - os: 'windows-latest' platform: 'win-amd64' runs-on: ${{matrix.os}} permissions: contents: write steps: - name: linux packages if: ${{ matrix.os == 'ubuntu-latest' }} run: | sudo apt-get update sudo apt install libasound2-dev libavcodec-dev libavformat-dev libavutil-dev libgl-dev libgtk-3-dev libpango1.0-dev libxtst-dev - name: Checkout the repository uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1 with: distribution: 'liberica' java-version: '21' java-package: 'jdk+fx' github-token: ${{ secrets.GITHUB_TOKEN }} cache: maven - name: Build shell: bash run: | ./mvnw -Pnative package - name: Archive Release uses: thedoctor0/zip-release@0.7.5 with: type: 'zip' filename: "raffle-${{ matrix.platform }}.zip" directory: target/native - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref }} release_name: native-image file: "target/native/raffle-${{ matrix. platform }}.zip" overwrite: true make_latest: true What do we have here? First of all, at the Build Native Image stage, we specify the OSs and platforms, for which to build a native image. Then, we add an additional step ‘linux packages’ with an if statement to install required packages for Linux. At this stage, we also specify a GraalVM distribution to compile the images. In our case, it is Liberica NIK for Java 21 that supports JavaFX. At the Build stage, we run ./mvnw -Pnative package to build the native images. At the Archive Release stage, we archive the native image specifying the platform in the file name. Here, we also specify the directory where the native image was created. Note that if the image will be generated by default in the target directory, the whole directory will be packaged into the ZIP file. So, that’s why we specified a separate directory for the native image in the pom.xml in the section above (target/native). At the Upload binaries to release stage, we take the archived native image and load it into the release. That’s it! Make sure that you created a release on GitHub and commit the changes: the workflow will be triggered automatically. After the workflow completes successfully, you will find the binaries for all platforms under Releases. Native Image release on GitHub Conclusion Creating JavaFX Native Images using GraalVM helps to improve the performance and portability of Java desktop applications by eliminating the need for a JVM at runtime. In this guide, we explored the entire process, from setting up GraalVM and configuring the necessary metadata to compiling the native executable and automating builds with GitHub Actions. If you want to explore JavaFX and native compilation further, check out the official GraalVM and Liberica Native Image Kit documentation for further optimizations. Also, subscribe to our letter for more content on JavaFX, native images, and all things Java! - [Liberica JDK 8u452, 11.0.27, 17.0.15, 21.0.7, and 24.0.1 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u452-11-0-27-17-0-15-21-0-7-and-24-0-1-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u461, 7u461, 8u451, 11.0.26.0.1, 17.0.14.0.1, 21.0.6.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u452, 11.0.27, 17.0.15, 21.0.7, and 24.0.1 with non-critical fixes and general improvements. The release contains 740 fixes and backports overall. BellSoft participated in eliminating 38 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 5 security issues (CVEs) fixed. 48 total security fixes (+ 40 additional non-security fixes) in CPU release: in Liberica 6u461: 15 security fixes + 5 additional fixes; in Liberica 7u461: 9 security fixes + 5 additional fixes; in Liberica 8u451: 6 security fixes + 11 additional fix; in Liberica 11.0.26.0.1: 6 security fixes + 6 additional fixes; in Liberica 17.0.14.0.1: 6 security fixes + 6 additional fixes; in Liberica 21.0.6.0.1: 6 security fixes + 7 additional fixes. In addition, PSU releases include a total of 652 fixes and backports: in Liberica 8u452: 6 security fixes (+ 4 in FX) + 33 additional fixes (+ 2 in FX); in Liberica 11.0.27: 6 security fixes (+ 4 in FX) + 31 additional fixes (+ 2 in FX); in Liberica 17.0.15: 6 security fixes (+ 6 in FX) + 240 additional fixes (+ 7 in FX); in Liberica 21.0.7: 6 security fixes (+ 6 in FX) + 233 additional fixes (+ 4 in FX). in Liberica 24.0.1: 6 security fixes (+ 2 in FX) + 38 additional fixes (+ 10 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2024-54534 7.5 javafx web network high none required unchanged high high high CVE-2024-47606 7.5 javafx media network high none required unchanged high high high CVE-2025-21587 7.4 security-libs javax.net.ssl network high none none unchanged high high none CVE-2025-30698 5.6 client-libs 2d network high none none unchanged low low low CVE-2025-30691 4.8 hotspot compiler network high none none unchanged low low none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 24 CVE-2024-54534 ● ● ● ● ● CVE-2024-47606 ● ● ● ● ● CVE-2025-21587 ● ● ● ● ● CVE-2025-30698 ● ● ● ● ● CVE-2025-30691 ● ● ● ● ● Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Creating Modern Desktop Apps with JavaFX and Spring Boot](https://bell-sw.com/blog/creating-modern-desktop-apps-with-javafx-and-spring-boot/): Introduction Desktop applications are far from obsolete in 2025. For industries such as Healthcare or Internet of Things, offline capabilities and responsive UIs still matter. So, if you want to create a desktop version of your application or have an idea for a rich client app with a responsive UI, JavaFX will be your faithful companion on this journey! It offers multiple graphics and media packages that enable writing cross-platform desktop interfaces Java style. But JavaFX not only interacts with any Java API, it can be integrated with Java frameworks such as Spring Boot, unlocking robust backend architecture, dependency injection, clear bean lifecycle management, centralized event handling, and many more cool Spring features. This will enable you to build complex, maintainable, scalable, responsive, and beautiful applications! In this article, we will explore why JavaFX and Spring Boot combo is a perfect match for modern desktop development. Or, if you are eager to start coding, you can jump right to the practice section where I show how to integrate these two technologies! Table of Contents Introduction What is JavaFX and Why Choose It for Desktop Applications? Why Integrate JavaFX with Spring Boot: Key Benefits Explained Setting Up the Project: JavaFX + Spring Boot from Scratch Project Initialization Create Spring Boot and JavaFX Application Classes How to Design Modern JavaFX UI with Scene Builder Key JavaFX Components Rapid UI Development with Scene Builder Integrating FXML File with the Application Code UI Styling with CSS Connecting JavaFX Frontend with Spring Boot Backend Load the FXML File Create the ApplicationConfig Class Create the StageManager Class to Manage Stage and All Scenes Use the StageManager Bean to Switch Between Scenes Building Backend with Spring Boot: Event Handling, and Database Connection Integrate Spring Data JPA Set Up SQLite Use ApplicationEvent to Manage Events Conclusion and Further Learning FAQ Frequently Asked Questions What is JavaFX and Why Choose It for Desktop Applications? JavaFX is a set of graphics and media packages for creating rich client applications in pure Java. JavaFX-based applications consistently run in desktop, mobile, and embedded environments. They can also interact with server applications. JavaFX was first introduced in 2008 as part of the Java Development Kit. Starting with Java 11, it was separated from the JDK into an open-source OpenJFX project. So, in order to use it, you need to add necessary dependencies or install a JDK that bundles with JavaFX such as Liberica JDK. But why should you use JavaFX for desktop development? Pure Java. JavaFX is fully Java-based meaning that you can write familiar code and use solutions from the Java ecosystem. Cross-platform. JavaFX applications run on Windows, Linux, and macOS. They can be embedded into a web page or other Java applications. Besides, they can be deployed to embedded systems, Raspberry PIs with a touchskreen, and ported to mobile devices. CSS and FXML. You can separate the frontend and backend thanks to CSS and FXML support and even use a separate tool called Scene Builder for creating UIs without coding. Out-of-the-box controls. JavaFX provides many common controls to build a fully-featured desktop app. Multitouch and multithreading. You can build highly responsive interactive applications as JavaFX supports multitouch operations. Rich multimedia capabilities. JavaFX supports 2D shapes, images, gradients, animations, 3D rendering, and custom graphics with Canvas API. Besides, it supports playback of common audio formats and video playback with support for various codecs and container formats. Thanks to these capabilities, solutions that you can build with JavaFX include: Business dashboards; HMI panels for industrial automation; Hospital device frontends; Interactive educational applications; Smart home controllers; Raspberry Pi interfaces. Why Integrate JavaFX with Spring Boot: Key Benefits Explained For all its might, JavaFX lacks modern dependency injection and bean lifecycle management. The business logic is built manually, which can make complex applications tangled and overcomplicated. In comes Spring Boot! Spring Boot is tailored to creating production-ready applications in a fast and efficient way. It provides a robust Inversion of Control mechanism, a set of starter dependencies to reduce manual configuration, automatic configuration of third-party dependencies, and directly embeddable servers. As a result, coupling JavaFX with Spring Boot will give you: Dependency injection to keep controllers, services, and DAOs clean and testable; Bean lifecycle management to centralize configuration and app logic; Centralized event management system; Spring-based data access model to implement familiar data management logic and reduce boilerplate code required for database connection. Therefore, you can have the best of two worlds: Beautiful and responsive UI with graphics and multimedia support from JavaFX; Centralized configuration management, dependency injection, and support for Spring ecosystem components from Spring Boot. Setting Up the Project: JavaFX + Spring Boot from Scratch The code for this tutorial is a simplified version of my pet project on GitHub. It is a work-in-progress educational application for learning the piano basics. The application is aimed at helping users learn and practice solfeggio concepts like chords, intervals, and scales through a virtual piano keyboard. Don’t worry, the code snippets I’m going to show here are not specific to the music learning program and can be used as a foundation for any app. Disclaimer: as JavaFX and Spring Boot are complex and flexible solutions, there’s no single approach to befriending them. What I’ll describe is one of the options tailored to scalability and sophisticated event management. Project Initialization First thing first, let’s create a new project. Go to Spring Initializr and create a bare-bone project based on the preferred Java version and build system. I’m using JDK 21 and Maven. Don’t add any dependencies just yet. With JavaFX, there are two options: you can add the dependencies for required OpenJFX packages or download a JDK that bundles JavaFX, such as Liberica JDK Full. In the latter case, you don’t have to add FX dependencies manually, which can be plentiful. They will be available out-of-the-box! You can also get Liberica JDK with JavaFX through SDKMAN: 21.0.7.fx-librca Set Liberica JDK Full as the Project SDK. Since we use Spring Boot, we have web support on the classpath. We don’t need it for JavaFX, so, disable the web server in the application.properties file: spring.main.web-application-type=none Now, the application is loaded with JavaFX and Spring Boot powers and is ready to use them for good! Create Spring Boot and JavaFX Application Classes You might be surprised, but we need two main classes in our application! The reason is that Spring Boot and JavaFX have their own startup cycle. Spring Boot has a main class annotated with @SpringBootApplication that starts the Spring ApplicationContext using SpringApplication.run(...) in the public static void main() method: @SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } } The ApplicationContext is responsible for spinning up all the backend logic: initializing beans, setting up services, repositories, and configurations. Therefore, Spring Boot expects a clean, standard main() method to bootstrap the entire context. On the contrary, the JavaFX main class extends javafx.application.Application and is required to launch the JavaFX UI thread, which is also known as the JavaFX Application Thread. It’s where the GUI is initialized — in the start(Stage primaryStage) method. JavaFX expects Application.launch() to be called from a class that extends Application, and it must run on its own thread. Consequently, we have to separate the main classes because JavaFX needs to control the UI thread and lifecycle. Spring Boot needs to initialize the backend context independently. They can’t both “own” the main() method unless you coordinate them. So, how do we make Spring Boot and JavaFX work together? Let’s create a class called CoolFxApplication — because, why not? — and make it extend javafx.application.Application: import javafx.application.Application; public class CoolFxApplication extends Application { } Next, we need to override several essential methods for starting and exiting the JavaFX application: @Override public void init() { } @Override public void stop() { } @Override public void start(Stage primaryStage) { } Next, add ConfigurableApplicationContext to the class fields: private ConfigurableApplicationContext applicationContext; We can now make the JavaFX Application class delegate to the Spring Boot context in the init() method, like this: @Override public void init() { applicationContext = new SpringApplicationBuilder(Main.class).run(); } @Override public void stop() { applicationContext.close(); } Finally, call Application.launch(...) from the public static void main() method of our @SpringBootApplication-annotated Main class: @SpringBootApplication public class Main { public static void main(String[] args) { Application.launch(CoolFxApplication.class, args); } } This approach keeps Spring Boot and JavaFX responsibilities clean and working in sync. Our job in setting up the Spring Boot Main class is done, but we’re not finished with the main JavaFX class! The whole point of making a desktop app is in providing the user with a beautiful and responsive interface, and we don’t have one yet. So, leave the CoolFxApplication class be for a while, we will continue configuring it after creating a user interface. How to Design Modern JavaFX UI with Scene Builder You can design and style a UI for a JavaFX application programmatically, with FXML, or CSS, and of course, you can combine all three. Key JavaFX Components JavaFX applications use the Scene Graph pattern for arranging a UI layout. In a Scene Graph, all components be it containers, figures, controls, or media objects are treated as nodes. Nodes can have a parent and children. A node that doesn’t have a parent is a root node. All the other nodes can have one parent. But all nodes can have one, several, or no children. The key components of JavaFX architecture: The javafx.stage.Stage represents a window of a JavaFX application. When the application starts, it creates a root Stage object. It is passed to the start(Stage primaryStage) method of the main JavaFX class. The primaryStage is the primary window of the application. The javafx.scene.Scene is placed onto the Stage to display the content; Containers are the layout panes. There are several out-of-the-box containers for setting up common layouts such as rows, stacks, etc; Controls such as buttons, text fields, check boxes, etc. support user interactions. You can also use built-in Charts, Shapes, or draw a custom node with Canvas. Rapid UI Development with Scene Builder FXML is an XML-based language for designing a UI layout. It helps to separate the layout code from the application code and keep to the MVC (Model — View — Controller) pattern. Good news is that you don’t need to write an FXML file yourself.You can download a Scene Builder application or install it as a plugin in IntelliJ IDEA and use its drag-and-drop interface to rapidly design a layout layer. If you have ever worked with Figma, the concept is familiar to you. Scene Builder has a laconic and user-friendly interface. The tabs on the left contain UI components and display the hierarchy of nodes. The tabs on the right let you style the nodes and bind them to the code. Scene Builder UI To learn more about working with Scene Builder, refer to the previous article. After you save the changes, the FXML file will be generated automatically. Of course, you are not limited to FXML and Scene Builder, especially if you need custom nodes. For instance, I usually use Scene Builder to draft a basic interface and then I style it in the CSS file and polish the functionality programmatically. If I need a component not provided out-of-the-box, I code it. This way, I reduce the time spent on designing a layout foundation and can focus on customizations. Alright, let’s create a couple of simple UIs. The first one is going to be a Login page where the user can sign in to their profile or sign in. We’ll keep it simple and use only username for login, but you can integrate Spring authentication and authorization capabilities and create a full fledged login form with a password. Drag a Pane to the center of the screen. Then, drag two Labels to the top of the Pane and change their text in the properties tab to “Your Personal Assistant Greets You!” and “Sign in with username or sign up:” Then, drag a TextField and drop it under the Labels. Finally, drag and drop two Buttons under the TextField and change their text properties to “Sign In” and “Sign Up.” The most important part is to bind these components to the code. Click on the TextField, go to the Code tab and enter userName under fx:id. For the Sign in button, enter signInButton under fx:id and loadUserAndOpenHomePage under onAction. This is going to be the name of the method in our code that handles the action performed with the button. For the Sign up button, enter signUpButton under fx:id and saveUserAndOpenHomePage under onAction. This is the resulting window: Login Page in Scene Builder Go to the Controller tab and enter org.java.coolfxdemo.controller.LoginController under controller class. We need to provide the reference path from Source Root. This is going to be the class that handles this particular window. Save the file as login.fxml in the resources directory of your project. Create a new file, drag and drop a Pane, then add a Label to it. Enter helloLabel under fx:id and org.java.coolfxdemo.controller.HomeController under controller class. Save the file as home.fxml to resources. Integrating FXML File with the Application Code Let’s create two classes that we specified in the FXML files. Create a new directory controller and two classes in this directory, LoginController and HomeController. These classes can extend the Initializable interface that initializes a controller after its root element has been processed. Keep in mind that it is not a Spring Boot controller, so no @Controller annotation! Instead, we need a @Component annotation so that Spring Boot recognizes these classes as beans. Add Button signInButton and Button signUpButton annotated with @FXML to the LoginController. In addition, create two methods, loadUserAndOpenHomePage() and saveUserAndOpenHomePage(): @Component public class LoginController implements Initializable { @FXML private Button signInButton; @FXML private Button signUpButton; public void loadUserAndOpenHomePage() { } public void saveUserAndOpenHomePage() { } @Override public void initialize(URL location, ResourceBundle resources) { } } Perform similar actions with HomeController: @Component public class HomeController implements Initializable { @FXML private Label helloLabel; @Override public void initialize(URL location, ResourceBundle resources) { } } We’ll add some meat to these classes later. Cool, we have linked the FXML files with the application code. What we need now is to load the files, extract the root node, place it on the stage, and make the app do something useful Spring Boot style. In and out, 20 minutes adventure! Let’s look briefly at UI styling before moving on. UI Styling with CSS The process of styling JavaFX nodes with CSS is similar to CSS styling applied to HTML DOMs. The CSS syntax is the same, but the properties have a bit different names prefixed with "-fx-". For instance, instead of the font-family JavaFX has -fx-font-family. Another example: the ":active" and ":focus" dynamic pseudo-classes are not supported. Instead, JavaFX supports ":pressed" and ":focused" pseudo-classes that have similar functionality. To learn more about JavaFX CSS styling, refer to JavaFX CSS Reference Guide. The stylesheet is applied to the Scene or its root node with the getStyleSheets() method. Styles can be applied to the root section to define global styles or to specific nodes. In the latter case, you can use the “.” selector to apply styles to the whole node type or the “#” selector to apply styles to a unique node with an id. For instance, to set the universal font for the application, the text color for all Labels, and the background color for the signUpButton, we can do the following: .root { -fx-font-family: Verdana; } .label { -fx-text-fill: #000000; -fx-wrap-text: true; } #signUpButton { -fx-background-color: #DBFFCB; } Our ultra-minimalistic UI is ready, let’s move on to the most exciting part of intertwining Spring Boot and JavaFX! Connecting JavaFX Frontend with Spring Boot Backend Load the FXML File Create a config directory. It will hold all our configuration classes. Firstly, create an enum FxmlView, whose single purpose will be to return the link to the required FXML file. We currently have two files, so the code will look like this: public enum FxmlView { LOGIN { @Override public String getFxmlPath() { return "/fxml/login.fxml"; } }, HOME { @Override public String getFxmlPath() { return "/fxml/home.fxml"; } }; public abstract String getFxmlPath(); } Later on, you can conveniently add more pages as your application grows. Secondly, create the FxmlLoader class and annotate it with @Component. This class will be responsible for loading FXML resources: @Component public class FxmlLoader { private final ApplicationContext context; public FxmlLoader(ApplicationContext context) { this.context = context; } public Parent load(String fxmlPath) throws IOException { FXMLLoader loader = new FXMLLoader(); loader.setControllerFactory(context::getBean); loader.setLocation(getClass().getResource(fxmlPath)); return loader.load(); } } As you can see, we make use of Spring Boot’s Dependency Injection to inject ApplicationContext. The class has a single method that receives a String with the path to the FXML file and loads the object hierarchy from it using a special FXMLLoader class of JavaFX. The loader also sets a controller factory for creating instances of Controller classes specified in the FXML. In our case, these controllers are Spring beans as we annotated them with @Component, so we ask the ApplicationContext to get a required bean. Create the ApplicationConfig Class Create the ApplicationConfig class annotated with @Configuration. Create the fxmlLoader and applicationTitle fields and inject these dependencies in the ApplicationConfig constructor. You can set the title for your application in the .properties file and use the @Value annotation for Spring to configure this property from the file: @Configuration public class ApplicationConfig { private final FxmlLoader fxmlLoader; private final String applicationTitle; public ApplicationConfig(FxmlLoader fxmlLoader, @Value("${application.title}") String applicationTitle) { this.fxmlLoader = fxmlLoader; this.applicationTitle = applicationTitle; } } Now, watch closely: we need to create a very special bean that will load the parent nodes from the FXML files, set them on the Scene, show the Stage, and switch Scenes when necessary. Let’s call it StageManager. This bean has to be instantiated in the ApplicationConfig class, but we must annotate it with @Lazy because the StageManager that receives a Stage in the constructor must be loaded after the Stage is created in the main application class: @Bean @Lazy public StageManager stageManager(Stage stage) throws IOException { return new StageManager(fxmlLoader, stage, applicationTitle); } What are we waiting for? Let’s create this super important bean! Create the StageManager Class to Manage Stage and All Scenes Create the StageManager class and annotate it with @Component. Add the primaryStage, fxmlLoader, and applicationTitle fields and inject them in the constructor: @Component public class StageManager { private final Stage primaryStage; private final FxmlLoader fxmlLoader; private final String applicationTitle; public StageManager(FxmlLoader fxmlLoader, Stage primaryStage, String applicationTitle) { this.primaryStage = primaryStage; this.fxmlLoader = fxmlLoader; this.applicationTitle = applicationTitle; } } Now, let’s create a switchScene() method, which Receives the FxmlView constant, Adds the application title to the stage with primaryStage.setTitle(applicationTitle), Loads the root node with Parent rootNode = loadRootNode(view.getFxmlPath()), Creates a new Scene object and sets the parent node on the Scene with Scene scene = new Scene(rootNode), Bootstraps the stylesheet, Sets the scene on the Stage with primaryStage.setScene(scene), and Shows the Stage with primaryStage.show(). public void switchScene(final FxmlView view) { primaryStage.setTitle(applicationTitle); Parent rootNode = loadRootNode(view.getFxmlPath()); Scene scene = new Scene(rootNode); String stylesheet = Objects.requireNonNull(getClass() .getResource("/styles/styles.css")) .toExternalForm(); scene.getStylesheets().add(stylesheet); primaryStage.setScene(scene); primaryStage.show(); } Let’s create a separate method for node loading: private Parent loadRootNode(String fxmlPath) { Parent rootNode; try { rootNode = fxmlLoader.load(fxmlPath); } catch (IOException e) { throw new RuntimeException(e); } return rootNode; } The switchScene() method will be called in the main JavaFX Application class when we first start the application. Afterwards, when we need to switch between Scenes, we will call another method that uses the Stage with all its settings from above and sets a new Scene onto it. We reuse the Stage object to avoid glitches when switching Scenes: public void switchToNextScene(final FxmlView view) { Parent rootNode = loadRootNode(view.getFxmlPath()); primaryStage.getScene().setRoot(rootNode); primaryStage.show(); } The complete code looks like this: @Component public class StageManager { private final Stage primaryStage; private final FxmlLoader fxmlLoader; private final String applicationTitle; public StageManager(FxmlLoader fxmlLoader, Stage primaryStage, String applicationTitle) { this.primaryStage = primaryStage; this.fxmlLoader = fxmlLoader; this.applicationTitle = applicationTitle; } public void switchScene(final FxmlView view) { primaryStage.setTitle(applicationTitle); Parent rootNode = loadRootNode(view.getFxmlPath()); Scene scene = new Scene(rootNode); String stylesheet = Objects.requireNonNull(getClass() .getResource("/styles/styles.css")) .toExternalForm(); scene.getStylesheets().add(stylesheet); primaryStage.setScene(scene); primaryStage.show(); } public void switchToNextScene(final FxmlView view) { Parent rootNode = loadRootNode(view.getFxmlPath()); primaryStage.getScene().setRoot(rootNode); primaryStage.show(); } private Parent loadRootNode(String fxmlPath) { Parent rootNode; try { rootNode = fxmlLoader.load(fxmlPath); } catch (IOException e) { throw new RuntimeException(e); } return rootNode; } } Use the StageManager Bean to Switch Between Scenes The StageManager bean is ready, let’s add it as a field along with the Stage to the CoolFxApplication class. In the start() method, initialize the Stage object by linking it to the primaryStage created by JavaFX. In addition, initialize the StageManager bean by retrieving it from the ApplicationContext. public class CoolFxApplication extends Application { private static Stage stage; private ConfigurableApplicationContext applicationContext; private StageManager stageManager; @Override public void init() { applicationContext = new SpringApplicationBuilder(Main.class).run(); } @Override public void stop() { applicationContext.close(); stage.close(); } @Override public void start(Stage primaryStage) { stage = primaryStage; stageManager = applicationContext.getBean(StageManager.class, primaryStage); } } The final touch: Create a method called showLoginScene() as the Login Page is the entry point to our app, and call the switchScene() method of the StageManager passing the required FxmlView constant, in this case, LOGIN: @Override public void start(Stage primaryStage) { stage = primaryStage; stageManager = applicationContext.getBean(StageManager.class, primaryStage); showLoginScene(); } private void showLoginScene() { stageManager.switchScene(FxmlView.LOGIN); } From now on, every time we start the application, the primary Stage will be created, after that, the StageManager bean will be instantiated via ApplicationConfig, and then retrieved in the CoolFxApplication class through the ApplicationContext to handle the initial Scene setup. You can now inject the dependency on StageManager to Controllers to switch between Scenes when a specific event is triggered. For example, in the LoginController class, we have two buttons that handle the user login and open the home page. So, we can inject the StageManager in the constructor and call its switchToNextScene() method in the loadUserAndOpenHomePage() and saveUserAndOpenHomePage() methods. Note that we must annotate the LoginController constructor with @Lazy to load it after the relevant Scene object is created: @Component public class LoginController implements Initializable { @FXML private Button signInButton; @FXML private Button signUpButton; @FXML private TextField userName; private final StageManager stageManager; @Lazy public LoginController(StageManager stageManager) { this.stageManager = stageManager; } public void loadUserAndOpenHomePage() { stageManager.switchToNextScene(FxmlView.HOME); } public void saveUserAndOpenHomePage() { stageManager.switchToNextScene(FxmlView.HOME); } @Override public void initialize(URL location, ResourceBundle resources) {} You can now run the application with the ./mvnw spring-boot:run command and make sure that it starts successfully and switches to the next scene when you click the buttons. Building Backend with Spring Boot: Event Handling, and Database Connection We successfully launched the application, but right now, it doesn’t do anything useful except for switching between scenes. Let’s fix that and add the database connection and event handling capabilities. Integrate Spring Data JPA You can set up a database connection with JavaFX even if you don’t use any framework, but in this case, you will have to do it pure Java style: establish the connection, write and execute the query, and so on. However, as we use Spring Boot, we can delegate all these tasks to Spring Data! I went with Spring Data JPA as it is the most optimal for my app’s architecture and purposes. You know the drill from now on. Add the JPA dependency to pom.xml: org.springframework.boot spring-boot-starter-data-jpa Create a User class annotated with @Entity: @Entity public class User { @Id @GeneratedValue private Long id; @Column(name = "user_name") private String userName; //constructors, getters, setters, equals(), and hashCode() } Create a UserRepository class that extends JpaRepository: public interface UserRepository extends JpaRepository { Optional findByUserName(String userName); } Create a UserService class annotated with @Service that works with data: @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public Optional findByUsername(String username) { return userRepository.findByUserName(username); } public void saveUser(String username) { User newUser = new User(); newUser.setUserName(username); userRepository.save(newUser); } } Nothing fancy, only Spring as we love it! Set Up SQLite Embedded databases are a good match for desktop applications as you want to store user’s data locally. I have chosen SQLite because it is a perfect match for desktop apps and embedded devices as it provides fast, efficient, reliable data management without the need for administration. Bootstrapping SQLite is fairly easy. You need to add two dependencies to the pom.xml. The first one is SQLite itself: org.xerial sqlite-jdbc 3.49.1.0 And the second one is Hibernate. As of Hibernate 6, SQLite dialect is supported, which is great because we don’t have to write the dialect ourselves: org.hibernate.orm hibernate-community-dialects In the application.properties file, configure the database connection properties: spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect driverClassName=org.sqlite.JDBC url=jdbc:sqlite:data/assistant.db username=sa password=sa spring.sql.init.mode=always spring.jpa.hibernate.ddl-auto=update spring.jpa.defer-datasource-initialization=false Finally, as Spring Boot doesn’t support SQLite configuration out-of-the-box, we have to do it manually. Create the DataSourceConfig class annotated with @Configuration that loads a DataSource bean with all required configs: @Configuration public class DataSourceConfig { @Autowired Environment env; @Bean public DataSource getDataSource() { final DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(Objects.requireNonNull(env.getProperty("driverClassName"))); dataSource.setUrl(env.getProperty("url")); dataSource.setUsername(env.getProperty("username")); dataSource.setPassword(env.getProperty("password")); return dataSource; } } That’s it! We integrated SQLite into our application, and now we can perform any CRUD operation. Let’s add the logic for saving and loading a user to our LoginController (don’t forget to add the dependency on UserService). Note that we are throwing a RuntimeException for simplicity sake. For real-world application, you will require more sensible error handling. @Component public class LoginController implements Initializable { @FXML private Button signInButton; @FXML private Button signUpButton; @FXML private TextField userName; private final StageManager stageManager; private final UserService userService; @Lazy public LoginController(StageManager stageManager, UserService userService) { this.stageManager = stageManager; this.userService = userService; } public void loadUserAndOpenHomePage() { Optional user = userService.findByUsername(userName.getText()); if (user.isEmpty()) { throw new RuntimeException(); } stageManager.switchToNextScene(FxmlView.HOME); } public void saveUserAndOpenHomePage() { String name = userName.getText(); Optional user = userService.findByUsername(name); if (user.isPresent() || name.length() > 20 || name.isBlank()) { throw new RuntimeException(); } userService.saveUser(name); stageManager.switchToNextScene(FxmlView.HOME); } } Run the application. It must save a new user and load the existing one. You can add custom exceptions to notify the user in case of incorrect input. Explore the project for examples. Use ApplicationEvent to Manage Events You can set up event handling by means of JavaFX or Spring features. In this tutorial, I’ll show you how to handle events Spring-style, but you can, of course, go with JavaFX capabilities. Our application is supposed to load custom user data after the user has signed in. User login qualifies as an event that needs to be handled. Let’s settle with a simple example for brevity's sake and instead of loading user settings, we will make the app greet the user on the home page by their username. It means that HomeController must receive the username from LoginController. Events can serve as data carriers between application classes. To add event handling support, we must modify our ApplicationConfig and StageManager. Add the dependency on org.springframework.context.ApplicationEventPublisher to ApplicationConfig and add it to the StageManager constructor. This ApplicationEventPublisher will be our centralized event handler. It will publish events, and other classes can listen to required events. @Configuration public class ApplicationConfig { private final FxmlLoader fxmlLoader; private final String applicationTitle; private final ApplicationEventPublisher eventPublisher; public ApplicationConfig(FxmlLoader fxmlLoader, @Value("${application.title}") String applicationTitle, ApplicationEventPublisher eventPublisher) { this.fxmlLoader = fxmlLoader; this.applicationTitle = applicationTitle; this.eventPublisher = eventPublisher; } @Bean @Lazy public StageManager stageManager(Stage stage) throws IOException { return new StageManager(fxmlLoader, stage, applicationTitle, eventPublisher); } } @Component public class StageManager { private final Stage primaryStage; private final FxmlLoader fxmlLoader; private final String applicationTitle; private final ApplicationEventPublisher eventPublisher; public StageManager(FxmlLoader fxmlLoader, Stage primaryStage, String applicationTitle, ApplicationEventPublisher eventPublisher) { this.primaryStage = primaryStage; this.fxmlLoader = fxmlLoader; this.applicationTitle = applicationTitle; this.eventPublisher = eventPublisher; } Next, create a LoginEvent class that extends ApplicationEvent. It must receive the username in its constructor, so that when the ApplicationEventPublisher creates this type of event, it will immediately become the carrier of the data we are interested in. public class LoginEvent extends ApplicationEvent { private String userName; public LoginEvent(Object source, String userName) { super(source); this.userName = userName; } public String getUserName() { return userName; } } The next step is to add the dependency on ApplicationEventPublisher to LoginController and attach the ChangeListener provided by JavaFX to the username value in the initialize() method. The ChangeListener is called whenever the value of the ObservedValue changes. We can use a Lambda function to add event handling logic to the changed() method of the ChangeListener. In this case, we are going to call the ApplicationEventPublisher to publish the LoginEvent. @Component public class LoginController implements Initializable { @FXML private Button signInButton; @FXML private Button signUpButton; @FXML private TextField userName; private final StageManager stageManager; private final UserService userService; private final ApplicationEventPublisher eventPublisher; @Lazy public LoginController(StageManager stageManager, UserService userService, ApplicationEventPublisher eventPublisher) { this.stageManager = stageManager; this.userService = userService; this.eventPublisher = eventPublisher; } @Override public void initialize(URL location, ResourceBundle resources) { userName.textProperty().addListener(new ChangeListener<>() { @Override public void changed(ObservableValue observable, String oldText, String newText) { eventPublisher.publishEvent(new LoginEvent(this, newText)); } }); } } The last thing we have to do is to add the method annotated with @EventListener to HomeController. It will listen to LoginEvent and dynamically set the value of Label. To change Label text dynamically, we must bind a StringProperty to it in the initialize() method. From now on, we can change the value of the StringProperty, which will be reflected in the Label value. The code looks like this: @Component public class HomeController implements Initializable { @FXML private Label helloLabel; private final StageManager stageManager; StringProperty nameProperty = new SimpleStringProperty(); @Lazy public HomeController(StageManager stageManager) { this.stageManager = stageManager; } @Override public void initialize(URL location, ResourceBundle resources) { helloLabel.textProperty().bind(nameProperty); } @EventListener public void handleLoginEvent(LoginEvent event) { nameProperty.setValue("Hello, " + event.getUserName() + "!"); } } Run the application. It should greet you by the name after you have logged in. Conclusion and Further Learning We explored how to combine the power of JavaFX and Spring Boot to build modern desktop applications. You can explore the full code on GitHub. We walked through integrating both technologies in a single project, using FXML and Scene Builder to create clean and modular UIs, styling with CSS, embedding SQLite for lightweight local storage, and leveraging Spring’s event system to organize application logic. This stack allows you to reuse your enterprise Java skills in desktop development and create interactive apps with a modern UI. Further learning sources: JavaFX Documentation JavaFX CSS Reference Guide Spring Boot Reference Guide Spring Events Documentation As for me, I will continue working on my Solfeggio Trainer and publish more articles on JavaFX and Spring Boot development. The next article will be about approaches to packaging and deploying JavaFX and Spring Boot applications. Subscribe to our newsletter so as not to miss these goodies! FAQ Frequently Asked Questions What are the alternatives to JavaFX? Some alternatives to JavaFX are Swing (older Java UI), SWT used in Eclipse, JavaScript-based Electron, and Qt with C++ or Python bindings. Is JavaFX compatible with all Java libraries? Yes, JavaFX can work with any standard Java library. How difficult is it for a beginner to learn JavaFX and Spring Boot? JavaFX is beginner-friendly for UI development, and Spring Boot simplifies backend logic. Therefore, learning both is not too challenging with the right tutorials. Which IDEs are best suited for desktop application development? You can use any IDE for desktop development. IntelliJ IDEA is a great choice because it has excellent JavaFX support, Scene Builder plugin, and Spring Boot integration out of the box. When to choose Electron or JavaFX? Choose Electron for building cross-platform apps with web tech. Pick JavaFX for Java-native apps that need strong performance, lower memory use, or deep Java backend integration. - [Liberica Native Image Kit 23.0.8, 23.1.7, and 24.2.1 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-8-23-1-7-and-24-2-1-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.8 for JDK 17, 23.1.7 for JDK 21, and 24.2.1 for JDK 24 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable improvements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2024-54534 7.5 javafx web network high none required unchanged high high high CVE-2024-47606 7.5 javafx media network high none required unchanged high high high CVE-2025-21587 7.4 security-libs javax.net.ssl network high none none unchanged high high none CVE-2025-30698 5.6 client-libs 2d network high none none unchanged low low low CVE-2025-30691 4.8 hotspot compiler network high none none unchanged low low none Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [30 Years with Java: A BellSoft Love Story](https://bell-sw.com/blog/30-years-with-java-a-bellsoft-love-story/): "Remember when we first downloaded that HotJava beta? Who could have imagined where we'd end up..." As Java turns 30, we're not here to recite another timeline of releases and features. Instead, we want to share something more personal: our three-decade love affair with a language that changed everything - including our lives. This is the story of Java through the eyes of our team — Aleksey, Valery and Peter, who literally built Java at Sun Microsystems; Dmitry and Pasha, who witnessed the ecosystem's explosive growth; and many others who've made Java their life's work. Table of Contents 1995: The Download That Changed Everything The Sun Microsystems Years: Building Java from the Inside The "Holy Sh*t" Moment: When Java Made Sense The Wild West Era: When Everyone Had Their Own JVM The Forgotten Heroes: Excelsior and the JVM Wars The Tools That Built Our Careers 2004: The Release That Changed Everything Going Open Source: The Transparency Revolution The Acquisition by Oracle: Fear and Survival The Lambda Awakening (2014) The Bloom of the Java Community Today: Still Writing the Story 1995: The Download That Changed Everything Java 1.0 had just been released, introducing the revolutionary concept of platform-independent programming. The web was still mostly static HTML, and the idea of interactive content running in browsers was groundbreaking. Valery: "I downloaded the first HotJava beta in '95. Thirty years ago! But I didn't start working with it professionally until 1997." Picture this: It's 1995, and here's this young engineer downloading something called "HotJava." No one knew what they were getting into. The web was just text and images, and suddenly there's this thing that can make websites interactive. Dmitry: "Java attracted me because it was new and being taught at university in the late 90s and early 2000s. You know how it is when you're learning - new always feels exciting." That excitement wasn't misplaced. We were witnessing the birth of interactive computing. The Sun Microsystems Years: Building Java from the Inside During the late 1990s, Java was rapidly evolving with major releases like J2SE 1.2 (1998) introducing Swing and expanding to mobile platforms. Sun Microsystems was pushing Java as the universal platform for everything from web browsers to enterprise applications. Some companies talk about Java expertise. We lived it. Peter and Valery weren't just using Java — they were literally building it at Sun Microsystems, working on the JDK itself. Valery: "We worked on JavaStudio — an IDE for Java, written in Java. It was one of the first such IDEs before Eclipse, NetBeans, and IntelliJ." JavaStudio was eventually cancelled, but they joined the main Java team. They even have commemorative plaques to prove it — actual souvenirs from Sun for their work on Java 1.1.7 to 1.4 and beyond. Not many companies can say their engineers have physical artifacts from Java's earliest days sitting on their shelves. Commemorative plaques for Java releases Peter: “Still working on the JDK today, over 25 years later. That's not just experience — that's dedication.” The "Holy Sh*t" Moment: When Java Made Sense As Java matured through the early 2000s, developers were discovering its true power: automatic memory management, built-in multithreading, and genuine "write once, run anywhere" capabilities that actually worked. Dmitry: "It was mind-blowing. You're writing something that looks like what you've seen before - like C - but there's a garbage collector doing all the heavy lifting. Multithreaded programming where you don't have to think about memory management. It was revolutionary." Pasha: "Suddenly you needed to think so much less than with most other popular languages back then. I came from Perl, which was also popular. Perl was like a puzzle you'd stare at, trying to assemble it in your head. But Java? Java was readable AND convenient to write." The Wild West Era: When Everyone Had Their Own JVM Before Java standardization took hold, the late 1990s and early 2000s saw every major tech company creating their own Java Virtual Machine implementation, leading to compatibility nightmares and fragmented ecosystems. Alex: "There were so many JVMs! It was total chaos. IBM made their own, everyone made their own. It was like a massive war." This wasn't just technical fragmentation - it was existential. Would Java survive as a unified platform? "Then the TCK (Technology Compatibility Kit) appeared, basically determining which companies could even use the word 'Java' on their websites. That's when things started to standardize.” The Forgotten Heroes: Excelsior and the JVM Wars Team: "Around 2000, there was this amazing company called Excelsior. They were incredible - in some ways still better than what Graal VM offers today. Huawei bought them in 2018 and shut down the product. But what a product! They just lacked marketing." These are the stories you won't find in official Java histories - the brilliant companies that pushed boundaries but didn't make it into the mainstream narrative. The Tools That Built Our Careers The early 2000s marked the explosion of Java tooling and frameworks. Build systems evolved rapidly, and the Spring Framework (launched in 2003) began its challenge to the complex Java EE standards. Team: "We started with Make-like build systems - just dumb scripts. Then Ant appeared for Java, which was revolutionary. Later came Ivy as a valuable addition to Ant." Pasha: "I started using Spring on version 2 in 2006. That means I completely missed the first major Spring release, which was purely to show that there could be good alternatives to Java EE. And now Spring 7 is coming out!" The ecosystem wasn't just evolving - it was exploding. Every new tool felt like a revelation. 2004: The Release That Changed Everything Java SE 5 (originally called Java 1.5) was a watershed moment, introducing modern language features that transformed Java from a verbose, basic language into something that felt contemporary and powerful. Aleksey: "Java SE 5 was huge! Not just because it was the first release some of us worked with as developers, but because it brought tremendous changes. Generics, enums, the new Java Memory Model, java.util.concurrent, varargs - these features made Java a truly modern language!" This wasn't just another update. This was Java growing up. Going Open Source: The Transparency Revolution Java's transition to open source (completed in 2006-2007) fundamentally changed how developers could interact with the language. Instead of trusting vendor documentation, developers could read the actual source code. Dmitry: "Real experts emerged who could say: 'I've actually seen the Java source code. I can explain why and how to configure it properly, what actually changed from release to release.'" When Java went open source, it wasn't just about code availability. It was about trust and understanding complexity. For many of us, it defined our path deeper into the Java ecosystem. The Acquisition by Oracle: Fear and Survival In 2009, Oracle acquired Sun Microsystems for $7.4 billion, taking control of Java's future. The community feared Oracle would commercialize or abandon Java, threatening the open ecosystem that had developed. Pasha: "The community wasn’t sure what to expect. Some thought that Oracle would simply close the project." Dmitry: "Sun was this unshakeable rock. They made hardware, they made the OS for their hardware, they made Java for their hardware. It seemed natural. Then suddenly - boom - Oracle bought everything." But here's the twist: Oracle didn't abandon Java. They invested into it and brought order into a huge community project. Sometimes, the community's fears don't come true. The Lambda Awakening (2014) After a painful 5-year gap between Java 6 and 7, Java 8 finally delivered the functional programming features developers had been craving. Lambda expressions and the Stream API revolutionized how Java code was written. Aleksey: "My dream came true when Java 8 released lambda support! I immediately stopped using for loops and replaced them with streams." We weren't alone in our enthusiasm. Java 8 is still heavily used in major institutions today. We even support it in our Performance Edition because the world isn't ready to let go. The Bloom of the Java Community With Java's new 6-month release cycle starting in 2017, the language continued evolving rapidly. Modern Java bears little resemblance to the verbose language many remember, with features like records, pattern matching, and improved performance keeping it competitive. After Java 8's release in 2014, we observed how the community around Java blossomed. Everything was blooming and thriving. New JDK distributions arrived, among them — our own Liberica JDK. Dmitry: “I will never forget the mixture of delight and excitement I felt when we first shipped Liberica JDK. We felt that it was the right thing to do, that we created something great that will be useful to developers. At the same time, it was scary, but in a good way, like when you are starting a life-long journey and don’t know what awaits you out there." The improvements and new features contributed by big companies and individual developers poured into the Java platform like summer rain, and numerous Java User Groups and Java-related conferences popped up all over the world with these rains. JUGs, conferences, seminars were buzzing with activity, spreading the word about Java, exchanging knowledge and experience, radiating ideas and driving Java forward. Java was ported to ARM, then to RISC-V, making itself at home on new platforms. It was also preparing to conquer the cloud as container support improved with every release. The BellSoft team took an active part in optimizing Java for the cloud. Among other contributions, we integrated Alpine Linux port into the mainline OpenJDK, which finally made it possible to create small container images. And today, Java is a rightful cloud dweller! So, if anybody asks you “What’s the secret behind Java’s vitality?”, you know the answer: the community! Today: Still Writing the Story It’s 2025. Within our company, some of our team members had started working with Java long before some of our colleagues were born. We've seen it grow from a curiosity downloaded on dial-up to the backbone of enterprise computing. And it never ceases to amaze us! The team reflects: "It's more than just a programming language. It's been our career, our passion, our daily companion for three decades." - [How to Use Spring Data MongoDB](https://bell-sw.com/blog/how-to-use-spring-data-mongodb/): Introduction: Why MongoDB with Spring Boot? MongoDB is an open-source cross-platform document-oriented database. Belonging to the family of NoSQL database solutions, it provides impressive scalability and flexibility to data-driven applications dealing with real-time analytics, IoT, operational intelligence, or e-commerce to name a few. Spring developers can benefit from integrating MongoDB into their projects without leaving the familiar grounds of the framework thanks to Spring Data MongoDB. In this guide, I will walk you through setting up MongoDB with Spring Boot, using a variety of its features, from indexes to aggregation. I will also demonstrate how to integrate Mongock for reliable database migrations. This guide is beginner-friendly but also includes some advanced topics, so, feel free to navigate to the section you are most interested in. The code used in the tutorial is available on GitHub. Table of Contents Setting Up MongoDB with Spring Boot Defining Your Data Models SQL vs NoSQL approach Basic Modelling Advanced Annotations Creating Mongo Repositories Basic CRUD Operations with MongoDB Using MongoTemplate and @Query for Custom Logic Projections and DTOs Aggregations with Spring Data MongoDB MongoDB Migrations with Mongock Why use migration tool with NoSQL What is Mongock Setup Mongock with Spring Boot Create Changelogs and Apply Updates Testing MongoDB Applications with @DataMongoTest and Testcontainers Testing Data Layer with @DataMongoTest and Testcontainers Integration Testing with Testcontainers MongoDB Alternatives Conclusion Setting Up MongoDB with Spring Boot The application we are building is called NeuroWatch. It is a cyberpunk-themed app aimed at collecting the data on civilians and cyberware they implanted, as well as live reports sent by implants and aggregates the data to monitor the implant health over the time. So much more fun than typical Student, User, and Author entities, right? Prerequisites: Spring Boot 3+ JDK 24 or at least JDK 17 supported by Spring Boot 3.x. I’m using Liberica JDK recommended by Spring. Your favorite IDE Docker and Docker Compose First, let’s create a new Spring Boot project. Head over to Spring Initializr and select three dependencies: Docker Compose, Spring Data MongoDB, and Testcontainers. Creating a new project Hit ‘Generate’ and open it in the IDE. Go to the Main application class and add the @EnableMongoRepositories annotation. @SpringBootApplication @EnableMongoRepositories public class MongodbDemoApp { public static void main(String[] args) { SpringApplication.run(MongodbDemoApp.class, args); } } The next essential step is to configure the MongoDB connection. You can either run a MongoDB server locally or in a container. I will spin up a MongoDB container using Docker Compose. Here’s the compose.yml file: services: mongodb: image: mongo:latest container_name: mongodb restart: unless-stopped environment: - MONGO_INITDB_DATABASE=neurowatch networks: - neurowatch_net ports: - "27017:27017" volumes: - mongo_data:/data/db-mongo-data command: "mongod --quiet --logpath /dev/null " healthcheck: test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ] interval: 10s timeout: 5s retries: 5 start_period: 10s volumes: mongo_data: networks: neurowatch_net: Let’s see briefly what is going on here: We specify the image we want to use (mongo:latest) restart:unless-stopped starts the container automatically if it is stopped not by the user. We specify the database name that mongo needs to create upon the first start (MONGO_INITDB_DATABASE=neurowatch) We provide the connection details such as the network and port The healthcheck part helps to verify that the container is functioning properly In the volumes part, we specify the path to the local data storage so that when you stop and restart the container image, your data is preserved. Next, specify the connection details in the application.properties file: spring.data.mongodb.host=localhost spring.data.mongodb.port=27017 spring.data.mongodb.database=neurowatch spring.data.mongodb.auto-index-creation=true Here, we specify the host, port, and database name. By default, user and password are not required. We also enable automatic index creation (more on indexes below). Before running the application, you need to start the mongodb container: docker-compose up -d That’s it, we are all set up for writing the actual application! Defining Your Data Models SQL vs NoSQL approach MongoDB is a NoSQL document-based database, which means that its approach to storing and retrieving data is different to that of relational databases. With traditional relational databases, we need to create a database schema beforehand and map the relationships between entities using foreign keys and joins. The data is stored in tables, where they form rows and columns. MongoDB stores entities in JSON-like documents. It enables the developers to nest arrays and sub-documents without first declaring a rigid schema. Accompanying information is stored together in one document and can be indexed for convenient and rapid access. There’s no need to predetermine database schema, MongoDB takes care of it. At the same time, data can be efficiently queried, sorted, aggregated, and filtered thanks to the MongoDB Query API. Keeping that in mind, let’s see how to create entities MongoDB-style! Basic Modelling Let’s start with basic modelling. We will have two collections: civilians mapped to the Civilian model and implant_logs mapped to the ImplantMonitoringLog. A collection in MongoDB is used to store data and is similar to tables in relational databases. The Implant class doesn’t need a separate collection: the implants will be stored in the Civilian model. Let’s define a Civilian class annotated with @Document. This annotation tells MongoDB that the class represents a MongoDB document. A MongoDB document is a self-contained “record” written in JSON-like syntax supporting more data types than plain JSON.The document can hold any set of key-value pairs, numbers, text, dates, arrays, or even nested documents. You can define the name of the collection in the annotation if it differs from the name of the class: @Document(collection = "civilians") public class Civilian { @Id private String id; private String legalName; private String nationalId; private LocalDate birthDate; private boolean criminalRecord; private boolean underSurveillance; private final LocalDateTime registeredInSystemAt; private List implants = new ArrayList<>(); // getters, setters, etc. are omitted for brevity } For now, we have only one annotated field. The @Id annotation specifies the primary key. In MongoDB, the id should have a type String, ObjectId, or UUID. If you want to define a custom name for a field, you can use a @Field annotation: @Field(name = "registered_at") private final LocalDateTime registeredInSystemAt; Now, let’s create the Implant class: public class Implant { private String type; private String model; private String version; private String manufacturer; private String serialNumber; private int lotNumber; private LocalDate installedAt; private final LocalDateTime registeredInSystemAt; // getters, setters, etc. are omitted for brevity } As you can see, it doesn’t have a separate collection in the database. You can also embed documents into other documents if required. Imagine we made Implant a separate Document for some reason, gave it the id and a collection. Then, you just add the list of implants to Civilian: @Document public class Implant { @Id private String id; } @Document public class Civilian { @Id private String id; private List implants; } Instead of embedding the document, you can reference it using the @DBRef annotation. Such reference will be eagerly resolved: @Document public class Implant { @Id private String id; } @Document public class Civilian { @Id private String id; @DBRef private List implants; } You can also use @DocumentReference instead of @DBRef to define more flexibly which fields of the embedded document should be loaded (the id field is referenced by default): @Document public class Implant { @Id private String id; } @Document public class Civilian { @Id private String id; @DocumentReference private List implants; } Either way, you should be cautious when embedding documents or referencing them. Updating deeply nested documents requires rewriting the entire document. In addition, eagerly fetched documents with @DBRef can impact the application performance, whereas lazy loading with @DBRef may complicate debugging. Advanced Annotations Moving on to more advanced annotations! Let’s start with indexing. MongoDB indexes are special data structures that facilitate querying data. They are similar to a book index that helps to find required content without reading each page. MongoDB indexes help to avoid a full collection scan to find necessary data. Indexes are created for frequently queried fields such as nationalId in Civilian. With an index, you can also specify the uniqueness of the field if necessary (although the uniqueness of the national id is debated in real world, let’s suppose that it is a unique number for the sake of this small demo): @Document(collection = "civilians") public class Civilian { @Indexed(unique = true) private String nationalId; } Indexes can be used not only with documents but with any entity whose data is preserved in the database. Let’s also create indexes for Implant: public class Implant { @Indexed(unique = true) private String serialNumber; @Indexed private int lotNumber; } Apart from single-field indexes, MongoDB supports compound (@CompoundIndex), text (@TextIndexed), geospatial (@GeoSpatialIndexed), and hashed (@HashIndexed) indexes. Compound indexes can help improve the performance of queries using criteria on multiple fields. They are defined at a class level. Let’s create a compound index of implantSerialNumber in ascending order and timestamp in descending order for ImplantMonitoringLog: @Document(collection = "implant_logs") @CompoundIndex(name = "implant_ts_idx", def = "{'implant_serial_number': 1, 'timestamp': -1}") public class ImplantMonitoringLog { } Geo-spatial queries is an exciting MongoDB feature that enables you to find documents within a given distance. We can’t go past it, I suggest we see it in action! Let’s add one more field to our ImplantMonitoringLog that will define the location it was created at: @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) private Point location; Here, Point is a class from org.springframework.data.geo, @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) annotation creates an index of type GEO_2DSPHERE that enforces usage of the $nearSphere operator when fetching the data. This operator takes into account the curvature of the Earth and performs a spherical, geodesic distance calculation. It enables you to search documents within a given radius and work with realistic distances on a globe. After adding this annotation, we can create repository methods for fetching ImplantMonitoringLog within a certain distance — I’ll show you how to do that in the next section. Another interesting set of annotations is audit annotations @CreatedDate and @LastModifiedDate. They help to keep track of document lifecycle for maintaining data traceability and enabling analytics or data versioning. Let’s add these annotations to our Civilian and ImplantMonitoringLog classes: @Document(collection = "civilians") public class Civilian { @CreatedDate private final LocalDateTime registeredInSystemAt; } @Document(collection = "implant_logs") public class ImplantMonitoringLog { @CreatedDate private final LocalDateTime timestamp; } Our models are ready, moving on to setting up repositories! Creating Mongo Repositories Spring Data MongoDB provides the MongoRepository interface, which, in turn, extends CrudRepository, QueryByExampleExecutor, and PagindAndSortingRepository. Therefore, it provides basic methods for CRUD operations on the entities, paging and sorting capabilities, and MongoDB-specific methods. Let’s create two repository interfaces annotated with @Repository, CivilianRepository and ImplantMonitoringLogRepository. Make them extend MongoRepository and specify the entity class name and id type: @Repository public interface CivilianRepository extends MongoRepository { } @Repository public interface ImplantMonitoringLogRepository extends MongoRepository { } At this point, we can already perform CRUD operations with methods provided internally by Spring repositories. In addition, we can use queries by convention. This feature enables the developers to define queries in method names following an established pattern. For instance, we can define a method for searching a civilian by nationalId as follows: @Repository public interface CivilianRepository extends MongoRepository { Optional findByNationalId(String nationalId); } We can also define methods for returning a List of ImplantMonitoringLog objects by implant serial number and a timestamp after or between specified dates: @Repository public interface ImplantMonitoringLogRepository extends MongoRepository { List findByImplantSerialNumber(String implantSerialNumber); List findByImplantSerialNumberAndTimestampAfter(String implantSerialNumber, LocalDateTime timestamp); List findByImplantSerialNumberAndTimestampBetween(String implantSerialNumber, LocalDateTime timestampFrom, LocalDateTime timestampTo); } Spring automatically parses these methods and generates the corresponding MongoDB queries. Remember we enabled geo-spatial queries with MongoDB? Let’s add a corresponding method to ImplantMonitoringLog repository: List findByLocationNear(Point point, Distance distance); The Near keyword in the method enables you to fetch all ImplantMonitoringLog within a given distance from the specified point. Of course, this method is for demonstration purposes only as in our application, there could be thousands of logs, which may seriously affect performance of the query. What you can do here is Specify additional parameters in the method to limit the number of search results. For instance, you can include a time window: List findByLocationNearAndTimestampBetween( Point point, Distance distance, LocalDateTime from, LocalDateTime to); Create an aggregation pipeline to filter and sort fetched documents. For such complicated queries or a more fine-grained control, we can use the @Query annotation or a MongoTemplate. We will discuss these approaches further on in the article. For now, let’s see how to perform basic CRUD operations with MongoDB. Basic CRUD Operations with MongoDB Create the CivilianService class and annotate it with @Service. This class will be responsible for communicating with the data access layer: @Service public class CivilianService { private final CivilianRepository civilianRepository; public CivilianService(CivilianRepository civilianRepository) { this.civilianRepository = civilianRepository; } } We can save a new Civilian document using the method save() or insert(). The save() method will insert a new document if it doesn't exist, or update the existing one if the id matches. Therefore, it can also be used for updating the entity. public Civilian saveCivilian(Civilian civilian) { return civilianRepository.save(civilian); } public Civilian updateCivilian(String id, Implant implant) { Civilian civilian = civilianRepository.findById(id).orElseThrow(); civilian.getImplants().add(implant); return civilianRepository.save(civilian); } On the other hand, insert() only adds a new document and will fail if the document already exists. The method can only be used for creating documents, but at the same time, it protects from accidental overwrites: public Civilian saveCivilian(Civilian civilian) { return civilianRepository.insert(civilian); } To find civilians, we can use in-built methods and the ones we defined in the interface: public Civilian getCivilianById(String id) { return civilianRepository.findById(id).orElseThrow(); } public Civilian getCivilianByNationalId(String nationalId) { return civilianRepository.findByNationalId(nationalId).orElseThrow(); } public List getAllCivilians() { return civilianRepository.findAll(); } Finally, we can delete civilians using in-built repository methods: public void deleteCivilian(Civilian civilian) { civilianRepository.delete(civilian); } public void deleteAllCivilians() { civilianRepository.deleteAll(); } For ImpantMonitoringLog, the process is similar. This is all very well, but let’s see how MongoDB shines in more complex querying cases. Using MongoTemplate and @Query for Custom Logic Sometimes, the method naming approach is not enough. You may want to query array elements, create complex filters, or avoid unreadable method names. If you want to go beyond the MongoRepository capabilities and define custom data querying and processing, you can use MongoTemplate or a @Query annotation. The @Query annotation allows you to define custom MongoDB queries directly in Repository interfaces. Using it is straightforward: you annotate a relevant repository method with a @Query and write a query using the BSON-based MongoDB query language. For instance, we want to find all civilians with implants whose lot number is greater than or equal to N: @Query("{ 'implants': { $elemMatch: { 'lotNumber': { $gte: ?0 } } } }") List findAllByImplantLotNumberGreaterThanEqual(int lotNumber); Here, implants refers to the embedded implants array inside Civilian; $elemMatch: filters for objects in the array that match the condition; $gte: ?0 checks for lotNumber greater than or equal to the method parameter. Refer to the official documentation for more details on the syntax. What about MongoTemplate? MongoTemplate is an API that provides an abstraction layer over the MongoDB operations. It enables you to create complex queries and aggregations without leaving the Spring programming model. When using MongoTemplate, we work with Query, Criteria, and Aggregation objects to define queries programmatically. Then, MongoTemplate translates these objects into BSON query documents. How do we use MongoTemplate? Well, we need to perform a few additional steps to enjoy programmatic querying. First, we need to define a custom repository interface, say, CivilianRepositoryCustom, where we define custom query methods: public interface CivilianRepositoryCustom { List findAllByImplantLotNumberGreaterThanEqual(int lotNumber); } After that, create a CivilianRepositoryCustomImpl class. The ‘Impl’ prefix is super important! This class will implement CivilianRepositoryCustom. Annotate this class with @Repository, inject the MongoTemplate bean, and override interface methods: @Repository public class CivilianRepositoryCustomImpl implements CivilianRepositoryCustom { private final MongoTemplate mongoTemplate; public CivilianRepositoryCustomImpl(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } @Override public List findAllByImplantLotNumberGreaterThanEqual(int lotNumber) { return null; } } Alright, the most interesting part begins here! We need to create a new Query object inside this method. This object accepts Criteria, which builds the required condition. In our case, we need to search in the implants array for an implant with a specific lot number. This lot number should be greater than or equal to the passed parameter. Finally, we need to pass this Query to MongoTemplate, which runs the query with its find(...) method and returns all matching civilians from the database. The code looks like that: @Override public List findAllByImplantLotNumberGreaterThanEqual(int lotNumber) { Query query = new Query(Criteria.where("implants.lotNumber").gte(lotNumber)); return mongoTemplate.find(query, Civilian.class); } The explanation is longer than the actual implementation 🙂 You can also write typesafe queries. One approach is to use the FluentMongo wrapper. As we used the same method for both examples, you can now compare the @Query-based approach with MongoTemplate. The question is, when to use which? Both methods are suitable for writing complex custom queries, so it is a matter of taste, really. Later on in the article we will also compare both approaches when writing aggregation pipelines. Up next: Projections and DTOs! Projections and DTOs MongoDB projection is the process of retrieving only those fields of the document that were specified instead of fetching the entire document. Projections help to reduce network traffic related to over-fetching and can protect against accidental data exposure. For instance, we want to retrieve only the legalName and nationalId of civilians. We can specify these fields using the @Query annotation: @Query(value = "{}", fields = "{ legalName : 1, nationalId : 1 }") List findAllLegalNamesAndIds(); This method returns a list of Civilian objects with the data on legalName and nationalId only, all the other fields will be null. A more sophisticated approach is to use interface-based projections. In this case, you define an interface specifying the fields you need, and Spring will automatically map query results to this projection. The example above can be adjusted as follows. Create an interface CivilianSummary with two getter methods for legalName and nationalId: public interface CivilianSummary { String getLegalName(); String getNationalId(); } Now, define a method in the CivilianRepository: List findAllByUnderSurveillance(boolean underSurveillance); As a result, Spring Data will return a CivilianSummary directly from the database. Interface-based projections bear a striking resemblance to DTOs, wouldn't you agree? Actually, they are not the same. DTOs or class-based projections are classes that you write manually, with fields, constructors, or even computed fields and custom logic. DTOs are more flexible as they let you define custom logic and easily handle deeply nested objects. On the other hand, interface-based projections are interfaces with getters for required fields. They are most suitable when you don’t need any custom logic in the resulting object. They are also associated with smaller overhead than DTOs because only the requested fields are fetched. Good news is that we can combine DTOs with projections and get the best of two worlds! Suppose we want to gather statistics on implant performance. For that purpose, we must calculate the average indicators for power usage, CPU usage, and neural latency over a given period of time. For that purpose, it would be better to create a DTO with corresponding fields. As such, let’s create a record MonitoringStats: public record MonitoringStats(String implantSerialNumber, double avgPowerUsageUw, double avgCpuUsagePct, double avgNeuralLatencyMs) { } We can now use it to hold the data we fetched and calculated. There’s just a small nuisance: to perform such a task, we need to master another powerful MongoDB tool, which is aggregations. So, follow me to the next section! Aggregations with Spring Data MongoDB Aggregation in MongoDB is the process of running a series of operations like filtering, grouping, or transforming data against a document directly on the database side. Aggregations are extremely useful when you need to perform analytics, gather statistics, or craft reports because they help to reduce data transfer and client-side computations. You can use out-of-the-box single-purpose aggregations like distinct() or countDocument(). In case you need to perform more complex computations, you can build your own aggregation pipeline. Aggregation pipeline is a series of operations performed on a set of data. You can create aggregation pipelines declaratively in a repository interface using a @Query annotation or programmatically with the help of MongoTemplate and Criteria. In our small demo, We already have two perfect use cases suitable for aggregation pipeline: We want to filter ImplantMonitoringLogs by time and distance, group them by implantSerialNumber, and transform them into a Map where implantSerialNumber is the key and a List of ImplantMonitoringLogs is a value. We want to calculate average values for implant performance metrics for a given implantSerialNumber and a time window, and return a MonitoringStats DTO as a query result. Well, what are we waiting for? Let’s get on to the tasks at hand! First, let’s prepare the grounds. We need to create the ImplantMonitoringLogRepositoryCustom interface with custom methods aggregateStats() and findLogsByAreaAndTimeGrouped(): public interface ImplantMonitoringLogRepositoryCustom { MonitoringStats aggregateStats(String serialNumber, LocalDateTime from, LocalDateTime to); public Map> findLogsByAreaAndTimeGrouped( Point center, double maxDistanceMeters, LocalDateTime from, LocalDateTime to); } Don’t forget to make the ImplantMonitoringLogRepository extend the new interface: public interface ImplantMonitoringLogRepository extends MongoRepository, ImplantMonitoringLogRepositoryCustom { } Next, create an ImplantMonitoringLogRepositoryCustomImpl class that implements the ImplantMonitoringLogRepositoryCustom interface and overrides its methods. In addition, inject the MongoTemplate bean: @Repository public class ImplantMonitoringLogRepositoryCustomImpl implements ImplantMonitoringLogRepositoryCustom { private final MongoTemplate mongoTemplate; public ImplantMonitoringLogRepositoryCustomImpl(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } } Shall we start with the aggregateStats() method? Once again, what we need to do is: Filter the logs and fetch only documents that match the given criteria: implant serial number and a time window; Calculate the average of certain fields: power usage, CPU usage, and neural latency; Create a projection that will contain only four fields: implant serial number and calculated metrics; Return a MonitoringStats object from the database. For the first operation, we need the MatchOperation class that will hold the given Criteria: MatchOperation match = Aggregation.match(Criteria.where("implantSerialNumber").is(serialNumber) .and("timestamp").gte(from).lte(to)); To calculate the average, we need the GroupOperation class. Here, we group logs by implantSerialNumber, calculate the average for each group, and give a new name to each resulting metric: GroupOperation group = Aggregation.group("implantSerialNumber") .avg("powerUsageUw").as("avgPowerUsageUw") .avg("cpuUsagePct").as("avgCpuUsagePct") .avg("neuralLatencyMs").as("avgNeuralLatencyMs"); Next, we need the ProjectionOperation class to rename _id to implantSerialNumber and round each average metric to 2 decimal places. ProjectionOperation project = Aggregation.project().and("_id").as("implantSerialNumber") .and(ArithmeticOperators.Round.roundValueOf("avgPowerUsageUw").place(2)).as("avgPowerUsageUw") .and(ArithmeticOperators.Round.roundValueOf("avgCpuUsagePct").place(2)).as("avgCpuUsagePct") .and(ArithmeticOperators.Round.roundValueOf("avgNeuralLatencyMs").place(2)).as("avgNeuralLatencyMs"); Finally, we combine all three stages into a single aggregation pipeline and ‘feed’ it to MongoTemplate, which executes the pipeline against the implant_logs collection and maps the result to a MonitoringStats object. Aggregation aggregation = Aggregation.newAggregation(match, group, project); AggregationResults results = mongoTemplate.aggregate( aggregation, "implant_logs", MonitoringStats.class); Full method implementation: Accordion header @Override public MonitoringStats aggregateStats(String serialNumber, LocalDateTime from, LocalDateTime to) { MatchOperation match = Aggregation.match(Criteria.where("implantSerialNumber").is(serialNumber) .and("timestamp").gte(from).lte(to)); GroupOperation group = Aggregation.group("implantSerialNumber") .avg("powerUsageUw").as("avgPowerUsageUw") .avg("cpuUsagePct").as("avgCpuUsagePct") .avg("neuralLatencyMs").as("avgNeuralLatencyMs"); ProjectionOperation project = Aggregation.project() .and("_id").as("implantSerialNumber") .and(ArithmeticOperators.Round.roundValueOf("avgPowerUsageUw").place(2)).as("avgPowerUsageUw") .and(ArithmeticOperators.Round.roundValueOf("avgCpuUsagePct").place(2)).as("avgCpuUsagePct") .and(ArithmeticOperators.Round.roundValueOf("avgNeuralLatencyMs").place(2)).as("avgNeuralLatencyMs"); Aggregation aggregation = Aggregation.newAggregation(match, group, project); AggregationResults results = mongoTemplate.aggregate( aggregation, "implant_logs", MonitoringStats.class); return results.getUniqueMappedResult(); } This aggregation pipeline is ready, one more to go! A quick reminder: we want to find all ImplantMonitoringLogs within a given radius and time window, group them by implant serial id, and return a map with key implantSerialNimber and value List. The first operation is already familiar to us: we use MatchOperation to filter logs by distance and time: MatchOperation match = Aggregation.match( Criteria.where("location").nearSphere(center) .maxDistance(maxDistanceMeters) .and("timestamp").gte(from).lte(to)); Then, we use the GroupOperation to group logs by implantSerialNumber and push each matching document into the logs array for that group: GroupOperation group = Aggregation.group("implantSerialNumber") .push(Aggregation.ROOT).as("logs"); After that, we create an aggregation pipeline, which is used by the MongoTemplate to return List: Aggregation aggregation = Aggregation.newAggregation(match, group); AggregationResults results = mongoTemplate.aggregate( aggregation, "implant_logs", Document.class); The next step is to iterate over each Document, get the implantSerialNumber and a list of Document logs. These logs can be then converted to the ImplantMonitoringLog class using the mongoTemplate.getConverter() method. Finally, we put a new entry into the Map, where implantSerialNumber is the key and List is a value: Map> grouped = new HashMap<>(); for (Document doc : results.getMappedResults()) { String serialNumber = doc.getString("_id"); List logsDocs = (List) doc.get("logs"); List logs = logsDocs.stream() .map(d -> mongoTemplate.getConverter() .read(ImplantMonitoringLog.class, d)) .toList(); grouped.put(serialNumber, logs); Full method implementation: Accordion header @Override public Map> findLogsByAreaAndTimeGrouped(Point center, double maxDistanceMeters, LocalDateTime from, LocalDateTime to) { MatchOperation match = Aggregation.match( Criteria.where("location").nearSphere(center) .maxDistance(maxDistanceMeters) .and("timestamp").gte(from).lte(to)); GroupOperation group = Aggregation.group("implantSerialNumber") .push(Aggregation.ROOT).as("logs"); Aggregation aggregation = Aggregation.newAggregation(match, group); AggregationResults results = mongoTemplate.aggregate( aggregation, "implant_logs", Document.class); Map> grouped = new HashMap<>(); for (Document doc : results.getMappedResults()) { String serialNumber = doc.getString("_id"); List logsDocs = (List) doc.get("logs"); List logs = logsDocs.stream() .map(d -> mongoTemplate.getConverter() .read(ImplantMonitoringLog.class, d)) .toList(); grouped.put(serialNumber, logs); } return grouped; } That was awesome! You can now proceed to writing the Controllers if you want to build a web application and take advantage of the logic we have written. As for this tutorial, there are two more topics left to discuss: testing and database migrations. MongoDB Migrations with Mongock Why use migration tool with NoSQL Using a database migration tool such as Liquibase or Flyway with SQL databases is justified as we need to write the schema and update it explicitly as needed. With MongoDB, the schema is generated automatically, and changes to the @Document class fields can be applied with a single write command. So, why bother with a migration tool? Even if MongoDB doesn’t enforce a rigid schema, your application needs it. The indexes, validation rules, and conventions must be correct and consistent across dev, CI, and prod. However, relying on auto-created schema may lead to Missing indexes resulting in slow queries, Incomplete validation rules leading to corrupted documents. You also might want to modify the data, which, in case of automatic updates, may result in mismatching schema versions in production. Therefore, a migration tool enables you to create version-controlled and idempotent change sets so that every environment starts with the same collections, indexes, and validations. What is Mongock Mongock is an open-source Java-based migration tool for NoSQL databases. It offers a code-first approach to schema generation meaning that you can write migration scripts in Java/Kotlin and ship them with your app. With Mongock you can Version changelogs, Create indexes and validation rules, Be sure of idempotent execution of change sets, Split documents, Seed sample data. Mongock is natively compatible with Spring/Spring Boot, so adding it to your app is just a matter of two dependencies and one annotation. Why not use the familiar tools such as Liquibase or Flyway? Liquibase/Flyway are tailored to relational databases. They have very limited and/or experimental support for MongoDB and BSON and don’t work with many Mongo-specific features such as geo-indexes. Therefore, Liquibase and Flyway remain gold standards for relational databases, whereas Mongock is a perfect fit for MongoDB and other non-relational DBs. Setup Mongock with Spring Boot Let’s add the dependencies for Mongock Runner and MongoDB driver to the pom.xml: io.mongock mongock-springboot io.mongock mongodb-springdata-v4-driver Now, add the annotation @EnableMongock to the main application class. This annotation triggers the Mongock runner upon application start to run the migrations: @EnableMongock @SpringBootApplication @EnableMongoRepositories public class MongodbDemoApp { public static void main(String[] args) { SpringApplication.run(MongodbDemoApp.class, args); } } Finally, add a new property to application.properties pointing to the location of changelog files: mongock.migration-scan-package=dev.cyberjar.migration It is important to note that if you want to create a schema manually with Mongock, you have to remove all @Indexed annotations from the classes or else MongoDB will attempt to create the indexes automatically, and migration will fail with an error that such index already exists. Create Changelogs and Apply Updates Note that this guide is applicable to Mongock version 5.x — some methods and flows were changed as compared to the previous major Mongock version. Create the SchemaDataInitializerChangeUnit class. Annotate it with @ChangeUnit and specify id, which will be stored in the ChangeUnit history collection, execution order, author (optional). @ChangeUnit(id = "schema-and-test-data", order = "001", author = "cyberjar") public class SchemaDataInitializerChangeUnit { private final MongoTemplate mongoTemplate; public SchemaDataInitializerChangeUnit(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } } First, let’s take care of creating the collections and indices. ChangeUnit classes can contain methods annotated with @BeforeExecution (optional) for executing operations such as DDL before the actual migration @RollbackBeforeExecution (obligatory if @BeforeExecution is present) reverts the changes made in the @BeforeExecution method, @Execution for the main migration method, @RollbackExecution for reverting changes made in the execution method. The creation of collections and indexes is a DDL operation and should be performed in the @BeforeExecution method. Firstly, let’s create the collections using the createCollection() method of MongoTemplate: @BeforeExecution public void beforeExecution() { mongoTemplate.createCollection("civilians"); mongoTemplate.createCollection("implant_logs"); } In the same method, create the indices. For that, we need the IndexOperations object that is bound to a specific collection by MongoTemplate. Using this object, we can create indices for the specified collection: IndexOperations civilianOps = mongoTemplate.indexOps("civilians"); civilianOps.createIndex( new Index().on("nationalId", Sort.Direction.ASC).unique()); Note that the new Index accepts the name and sorting direction. You can also specify additional properties of this index. For instance, whether it is unique or not. In a similar way, let’s create the indices for the implants. As we don’t have the separate collection for them, we bind the index to the class: IndexOperations implantOps = mongoTemplate.indexOps(Implant.class); implantOps.createIndex( new Index().on("serialNumber", Sort.Direction.ASC).unique()); implantOps.createIndex( new Index().on("lotNumber", Sort.Direction.ASC)); As for the ImplantMonitoringLogs, we need to create a GeospatialIndex indeed of the standard Index for the location field and specify its time. We can also make the timestamp not just an Index, but a TTL Index. MongoDB uses such indices to remove the documents from the collection after a specified amount of time. I believe in the case of logs, a TTL index is more beneficial because otherwise, the amount of logs in the collection may exceed the sensible numbers. IndexOperations logOps = mongoTemplate.indexOps("implant_logs"); logOps.createIndex( new Index().on("implantSerialNumber", Sort.Direction.ASC)); logOps.createIndex( new Index().on("timestamp", Sort.Direction.DESC)); logOps.createIndex( new GeospatialIndex("location") .typed(GeoSpatialIndexType.GEO_2DSPHERE)); logOps.createIndex( new Index() .on("timestamp", Sort.Direction.ASC) .expire(Duration.ofDays(90))); We also need a @RollbackBeforeExecution method is something goes wrong during schema creation: @RollbackBeforeExecution public void rollbackBeforeExecution() { mongoTemplate.dropCollection("civilians"); mongoTemplate.dropCollection("implant_logs"); } We can now move on to the @Execution method to seed some test data into our database: Accordion header @Execution public void seedDatabase(MongoTemplate mongoTemplate) { List implants = new ArrayList<>(); implants.add(new Implant("limb", "Model-Dvb688", "2.2", "MechaMed", 536, "742669", "2025-03-21")); implants.add(new Implant("ocular", "Model-SiT679", "1.5", "MechaMed", 434, "306310", "2025-06-08")); implants.add(new Implant("limb", "Model-Jtv413", "1.3", "MechaMed", 536, "470917", "2025-04-03")); List civilians = new ArrayList<>(); civilians.add(new Civilian(null, "Aarav Das", "Ni-96751543-BP", "1965-05-02", true, false, List.of(implants.get(0)))); civilians.add(new Civilian(null, "Paula Lin", "NP-59909166-Wg", "1998-11-01", false, false, List.of(implants.get(1)))); civilians.add(new Civilian(null, "Aelita Fang", "gQ-01247486-nk", "1989-12-01", true, false, List.of(implants.get(3)))); mongoTemplate.insert(civilians, Civilian.class); String implantSerialNum = implants.getFirst().getSerialNumber(); String civilianNationalId = civilians.getFirst().getNationalId(); double powerUsage = 1.5; double cpuUsage = 1.0; double neuralLatency = 0.5; List logs = new ArrayList<>(); for (int i = 0; i < 30; i++) { ImplantMonitoringLog implantMonitoringLog = new ImplantMonitoringLog(null, implantSerialNum, civilianNationalId, LocalDateTime.now().minusHours(i), powerUsage + i, cpuUsage + i, neuralLatency + i, new Point(4.899, 52.372)); //Coordinates for Amsterdam longitude/latitude logs.add(implantMonitoringLog); } mongoTemplate.insert(logs, ImplantMonitoringLog.class); } Finally, the @RollbackExecution method will revert the changes of the migration method if necessary. You can delete all documents from the collections or simply drop the collections: @RollbackExecution public void rollbackExecution() { mongoTemplate.dropCollection("civilians"); mongoTemplate.dropCollection("implant_logs"); } Testing MongoDB Applications with @DataMongoTest and Testcontainers Our small demo app is ready, it’s time to test it! Testing Data Layer with @DataMongoTest and Testcontainers Spring Boot provides the @DataMongoTest for testing the data layer of the application without the full auto-configuration. When applied at the test class level, it searches for MongoDB-specific beans like repositories and documents and configures only them, leaving services, controllers, etc. out of the equation. By default, tests annotated with @DataMongoTest use the embedded MongoDB database. But we will override this behavior and use dockerized MongoDB, which we will spin up with the help of Testcontainers. Why would we want to do that? Unlike the embedded database, Testcontainers provide the actual database instance in a Docker container meaning that your tests will run against the realistic production-like environment. We have already added the Testcontainers support when we created the project. We need to add one more, for JUnit. All three dependencies: org.testcontainers junit-jupiter 1.21.0 test org.testcontainers mongodb 1.21.0 test org.springframework.boot spring-boot-testcontainers test Now, let’s create a test class for CivilianRepository and annotate it with @Testcontainers, which delegates the lifecycle of containers to Testcontainers, and @DataMongoTest: @Testcontainers @DataMongoTest class CivilianRepositoryTest { } Now, we need to create the instance of a MongoDBContainer using the specified Docker image. The @ServiceConnection annotation allows the MongoDB-related beans to communicate with MongoDB inside the Docker container. Also, autowire the CivilianRepository bean that we will test and the MongoTemplate bean that will be responsible for adding test data to the database. @Testcontainers @DataMongoTest class CivilianRepositoryTest { @Container @ServiceConnection static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo"); @Autowired private CivilianRepository repository; @Autowired private MongoTemplate mongoTemplate; } The final step is to disable Mongock for this set of tests. You can and should test migrations separately, but in other tests, Mongock is not necessary and will only complicate the setup. Create the test.properties file and add one property to disable Mongock: mongock.enabled=false Now, specify the path to the test.properties file with the class-level @TestPropertySource annotation: @Testcontainers @DataMongoTest @TestPropertySource(locations = "classpath:test.properties") class CivilianRepositoryTest { } Excellent. Now, let’s add some sample data to the containerized database. The best practice is to isolate the tests from one another so that they don’t interfere with each other’s results. To achieve that, we can perform database cleanup with a subsequent data insert after each test. Use the @BeforeEach and @AfterEach annotations: @BeforeEach void populateWithData() { mongoTemplate.createCollection("civilians"); List implants = new ArrayList<>(); implants.add(new Implant("limb", "Model-Dvb688", "2.2", "MechaMed", 536, "742669", "2025-03-21")); implants.add(new Implant("ocular", "Model-SiT679", "1.5", "MechaMed", 434, "306310", "2025-06-08")); implants.add(new Implant("limb", "Model-Jtv413", "1.3", "MechaMed", 536, "470917", "2025-04-03")); List civilians = new ArrayList<>(); civilians.add(new Civilian(null, "Rin Morse", "fI-88901036-kD", "1985-08-01", true, false, List.of(implants.get(0)))); civilians.add(new Civilian(null, "Heather Huang", "YD-99086969-CP", "1994-04-16", false, true, List.of(implants.get(1)))); civilians.add(new Civilian(null, "Amir Morgan", "MP-66879496-vg", "1975-06-26", false, true, List.of(implants.get(2)))); mongoTemplate.insert(civilians, Civilian.class); } @AfterEach void cleanUp() { mongoTemplate.dropCollection("civilians"); } In the example above, we added data manually. Another approach is to create a JSON file with all required data and add the Jackson2RepositoryPopulatorFactoryBean to the config file. This bean will populate the database with data when the container starts if we import it to the test class: @Configuration public class PopulatorConfig { @Bean public Jackson2RepositoryPopulatorFactoryBean populator() { var bean = new Jackson2RepositoryPopulatorFactoryBean(); bean.setResources(new Resource[] { new ClassPathResource("mydata.json") }); return bean; } } @DataMongoTest @Import(PopulatorConfig.class) class DataMongoTestWithJson { // Repos and tests as needed } Finally, we can use the familiar flow to write some tests: @Test void shouldFindCivilianByNationalId() { Optional civilian = repository.findByNationalId("fI-88901036-kD"); String name = "Rin Morse"; assertEquals(name, civilian.get().getLegalName()); } @Test void shouldFindCiviliansByLotNumber() { List civilians = repository.findAllByImplantLotNumber(536); int expected = 2; assertEquals(expected, civilians.size()); } Integration Testing with Testcontainers After we have tested all data classes in isolation, we can move on to integration testing. Integration testing is aimed at verifying that different parts of the application work correctly together. Let’s create a test class for our ImplantMonitoringLogService. We need the @Testcontainers annotation and the @SpringBootTest annotation instead of @DataMongoTest to use the whole application context. The container setup can be copied from the previous test class: @Testcontainers @SpringBootTest(classes = MongodbDemoApp.class) @TestPropertySource(locations = "classpath:test.properties") class ImplantMonitoringLogServiceTest { @Container @ServiceConnection static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo"); @Autowired private ImplantMonitoringLogService monitoringLogService; @Autowired private MongoTemplate mongoTemplate; } Let’s add some test data to the database. Again, we are populating and dropping the database for each test: @BeforeEach void populateWithData() { mongoTemplate.createCollection("implant_logs"); String implantSerialNum = "123456qw"; String civilianNationalId = "rtfg5674-98"; double powerUsage = 1.5; double cpuUsage = 1.0; double neuralLatency = 0.5; List logs = new ArrayList<>(); for (int i = 0; i < 30; i++) { ImplantMonitoringLog implantMonitoringLog = new ImplantMonitoringLog(null, implantSerialNum, civilianNationalId, LocalDateTime.now().minusHours(i), powerUsage + i, cpuUsage + i, neuralLatency + i, new Point(4.899, 52.372)); //Coordinates for Amsterdam logs.add(implantMonitoringLog); } mongoTemplate.insert(logs, ImplantMonitoringLog.class); } @AfterEach void cleanUp() { mongoTemplate.dropCollection("implant_logs"); } Finally, you can write corresponding methods to test the application logic: @Test void shouldGatherStatsForImplantLogs() { MonitoringStats stats = monitoringLogService.aggregateStatsForImplantForPeriod( "123456qw", LocalDateTime.now().minusDays(7), LocalDateTime.now() ); double expectedAvgPowerUsage = 16.0; assertEquals(expectedAvgPowerUsage, stats.avgPowerUsageUw()); } MongoDB Alternatives MongoDB is a powerful and flexible NoSQL solution, but it is not the only one on the market. Here are some MongoDB alternatives that may fit your needs better: Couchbase is a distributed NoSQL database platform that offers built-in caching for lower latency SQL-like query language (N1QL) for JSON documents. Apache Cassandra is a highly-scalable NoSQL solution designed for high availability and big data management. It boasts multi-center support and fault-tolerance by default. Amazon DynamoDB is a fully-managed NoSQL solution that is tightly integrated with the AWS ecosystem. I would recommend studying carefully what each solution offers and try it out to find which of them fits your needs best. Conclusion A quick summary? If you’ve read this far, you can now Set up MongoDB with Spring Boot and perform CRUD operations Use @Query annotation and MongoTemplate for more complex business logic Use specific MongoDB annotations for robust and efficient schema Use Projections and build Aggregation Pipelines Write migration scripts with Mongock Perform data layer and integration testing of MongoDB apps with @DataMongoTest and Testcontainers If you’d like to deepen your knowledge of MongoDB, you can further explore the docs: MongoDB Docs Spring Data MongoDB Docs MongoDB Aggregation Framework Reactive MongoDB And of course, subscribe to our newsletter for a deep dive into other cool technologies around Java - [Liberica JDK 8u462, 11.0.28, 17.0.16, 21.0.8, and 24.0.2 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u462-11-0-28-17-0-16-21-0-8-and-24-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u471, 7u471, 8u461, 11.0.27.0.1, 17.0.15.0.1, 21.0.7.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u462, 11.0.28, 17.0.16, 21.0.8, and 24.0.2 with non-critical fixes and general improvements. The release contains 985 fixes and backports overall. BellSoft participated in eliminating 12 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 7 security issues (CVEs) fixed. 31 total security fixes (+ 4 additional non-security fixes) in CPU release: in Liberica 6u471: 2 security fixes + 4 additional fixes; in Liberica 7u471: 4 security fixes; in Liberica 8u461: 6 security fixes; in Liberica 11.0.27.0.1: 7 security fixes; in Liberica 17.0.15.0.1: 6 security fixes; in Liberica 21.0.7.0.1: 6 security fixes. In addition, PSU releases include a total of 950 fixes and backports: in Liberica 8u462: 6 security fixes (+ 3 in FX) + 27 additional fixes (+ 2 in FX); in Liberica 11.0.28: 7 security fixes (+ 3 in FX) + 31 additional fixes (+ 1 in FX); in Liberica 17.0.16: 6 security fixes (+ 3 in FX) + 294 additional fixes (+ 2 in FX); in Liberica 21.0.8: 6 security fixes (+3 in FX) + 362 additional fixes (+ 4 in FX). in Liberica 24.0.2: 8 security fixes (+ 3 in FX) + 166 additional fixes (+ 13 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2025-50059 8.6 core-libs java.net network low none none unchanged high none none CVE-2025-30749 8.1 client-libs 2d network high none none unchanged high high high CVE-2025-50106 8.1 client-libs 2d network high none none unchanged high high high CVE-2025-30761 5.9 core-libs javax.script network high none none unchanged none high none CVE-2025-30754 4.8 security-libs javax.net.ssl network high none none unchanged low low none CVE-2025-27113 7.5 javafx web network high none required unchanged high high high CVE-2025-24855 7.5 javafx web network high none required unchanged high high high Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 24 CVE-2025-50059 ● ● ● ● CVE-2025-30749 ● ● ● ● ● CVE-2025-50106 ● ● ● ● ● CVE-2025-30761 ● ● CVE-2025-30754 ● ● ● ● ● CVE-2025-27113 ● ● ● ● ● CVE-2025-24855 ● ● ● ● ● Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Liberica Native Image Kit 23.0.9, 23.1.8, and 24.2.2 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-9-23-1-8-and-24-2-2-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.9 for JDK 17, 23.1.8 for JDK 21, and 24.2.2 for JDK 24 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable improvements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2025-50059 8.6 core-libs java.net network low none none unchanged high none none CVE-2025-30749 8.1 client-libs 2d network high none none unchanged high high high CVE-2025-50106 8.1 client-libs 2d network high none none unchanged high high high CVE-2025-30761 5.9 core-libs javax.script network high none none unchanged none high none CVE-2025-30754 4.8 security-libs javax.net.ssl network high none none unchanged low low none CVE-2025-27113 7.5 javafx web network high none required unchanged high high high CVE-2025-24855 7.5 javafx web network high none required unchanged high high high Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [BellSoft Upgrades Liberica JDK Performance Edition with JVM 21](https://bell-sw.com/news/bellsoft-upgrades-liberica-jdk-performance-edition-with-jvm-21/): BellSoft Supercharges Legacy Java with JVM 21 in Liberica JDK Performance Edition Upgrade Delivers faster startup, lower latency, and better throughput for legacy Java apps San Jose, CA — July 22, 2025 — BellSoft, the creator of Liberica JDK, a progressive Java runtime for the most complete Java experience and a leading OpenJDK contributor, today announced a major upgrade to Liberica JDK Performance Edition, now powered by the Java Virtual Machine (JVM) from JDK 21. The enhanced release delivers significant performance gains for organizations still running Java 8 and 11 with no code changes or migration required. “This upgrade is about future-proofing without the hassle,” said Alexander Belokrylov, co-founder and CEO of BellSoft. “Many companies are still running business-critical applications on Java 8 or 11 not because they’re behind, but because those platforms are stable, proven, and deeply integrated into their workflows. With Liberica JDK Performance Edition, we’re giving those teams a drop-in way to unlock the performance of JDK 21 without rewriting a single line of code. It’s a practical, engineering-first approach that respects the realities of enterprise development.” While newer Long-Term Support (LTS) versions of Java offer steady improvements, many organizations continue to rely on older releases. As of 2025, 28.8% of businesses still run Java 8, and JDK 11 remains widely used. In BellSoft’s 2024 survey, two-thirds of developers reported that they still support applications running on Java 11 or earlier. Nearly a quarter of respondents said they allocate additional budget to improve performance on those older runtimes. With Liberica JDK Performance Edition, teams can boost performance without rewriting code or spending resources on costly workarounds. Originally launched in April 2024, Liberica JDK Performance Edition was built to give legacy Java applications a serious performance boost. The latest version takes it up a notch, baking the core JVM and HotSpot from JDK 21 directly into builds of JDK 8 and 11, giving enterprises the benefits of modern Java runtime performance while maintaining plug-and-play simplicity. Key Improvements in the Upgrade Upgraded from JDK 17 to JDK 21 JVM core for JDK 8 and 11 builds Delivers 5 to 10% better performance in most workloads, with gains of up to 40% in select cases. Faster startup, reduced latency, and 2x+ compression speed with zlib-ng, a next-generation zlib data compression library designed for modern systems that delivers unprecedented performance improvements compared to OpenJDK packages using standard zlib. Performance and Runtime Enhancements Liberica JDK Performance Edition now includes production-ready ZGC with generational support, optimized G1GC, NUMA-aware tuning, and runtime tools like AppCDS and segmented code cache. Advanced compression via zlib-ng delivers real gains for I/O-intensive applications. Benchmark Results Liberica JDK Performance Edition was tested against standard OpenJDK builds using the Spring PetClinic as a reference application. The results showed significant gains across multiple performance metrics. Compression speed more than doubled, with a 126 percent improvement in compression and a 149 percent improvement in decompression. In enterprise workload testing using SpecJBB, throughput nearly doubled, with a 100 percent increase in Critical JOps and a 43 percent boost in MaxJOps when running G1GC on small-memory configurations. Latency in memory-intensive operations was also reduced by nearly 100 percent when switching from JDK 8 G1GC to the Performance Edition with ZGC. These improvements demonstrate clear advantages for applications that rely on high-throughput, efficient compression, and low-latency performance. For full benchmark details and technical documentation, visit BellSoft Liberica JDK Performance Edition. Availability Liberica JDK Performance Edition is available now for JDK 8 and JDK 11 on both x86_64 and AArch64 architectures, with builds supporting all major operating systems, including Linux, Windows, and macOS. Each version is powered by the Java Virtual Machine (JVM) from JDK 21. Support for additional configurations and environments is available on demand. Resources Download Liberica JDK, the best alternative to Oracle JDK Forbes Council: BellSoft CEO and co-founder Alex Belokrylov on Java's 30th Anniversary—A Look At Years Of Sun And Clouds Join our live webinar on July 23 at 7:00 PM CEST: “It’s Fine, Actually: Doing Better in Legacy Java.” Pasha Finkelshteyn, Developer Advocate at BellSoft, and Baruch Sadogursky, Developer Advocate at TuxCare, will share practical tips on writing cleaner, more modern-feeling Java even when you’re working with older language versions. Register at https://bell-sw.com/webinars/java8-fine-actually/ - [BellSoft Released Liberica JDK Performance Edition with JVM 21](https://bell-sw.com/blog/bellsoft-released-liberica-jdk-performance-edition-with-jvm-21/): We are happy to announce the release of Liberica JDK Performance Edition, which integrates JVM 21. The solution is aimed at helping enterprises running their projects on JDK 8 or 11 achieve a significant performance boost without code refactoring. Liberica JDK Performance Edition is available for JDK 8 and JDK 11 running on Linux, Windows, and macOS for x86_64 and AArch64. Support for other platforms is available on demand. Explore Liberica JDK Performance Edition When Immediate Java Upgrade is Not an Option Many enterprises still use JDK 8 or 11 to run their Java applications. These releases are deeply integrated into their infrastructure, and an immediate upgrade would result in a critical loss of system stability. Despite their stability, these versions are in deep maintenance, which means that few or no performance improvements or new features are ported to these OpenJDK branches. As such, the application cannot meet modern performance requirements. To meet new system requirements, you can add more hardware or upgrade to modern Java versions that utilize hardware and handle growing workloads more efficiently. Neither approach is easy to implement. Adding more hardware is costly and only temporarily improves performance because the application will demand more capacities as workloads grow. On the other hand, upgrading the Java version may require significant code refactoring and updating numerous libraries the application uses, which may result in more refactoring. While the developers solve all compatibility issues, the application performance may be severely degraded. But even if the upgrade is not coming soon, staying on JDK 8 or 11 doesn’t mean suffering. It is possible to run applications on legacy Java and still get optimal results and use good engineering practices. To learn more about keeping legacy systems in good shape, watch the webinar with Pasha Finkelshteyn, Developer Advocate at BellSoft, and Baruch Sadogursky, Developer Advocate at TuxCare, where they share techniques on making legacy Java apps safer, faster, and easier to maintain. To boost systems' performance without upgrades, Liberica JDK Performance Edition offers a solution. It enables companies to stay on legacy JDK but benefit from the performance perks of modern JVM without code refactoring. What is Liberica JDK Performance Edition Liberica JDK Performance Edition, or liberica-perf for short, is a version of Liberica JDK that seamlessly integrates legacy JDK 8 or 11 and HotSpot JVM from newer Java versions. This design enables you to change only one component of the enterprise stack without breaking anything else, as in a complete migration. As a result, you can migrate to Liberica JDK Performance Edition with little to no code refactoring and benefit from the immediate performance boost: 5-10% general performance improvement in most cases, and up to 40% in edge cases. 10% faster application response time in most cases. Up to 7% better startup. Two times faster compression/decompression for ZIP format. Thanks to these performance improvements, it is possible to optimize hardware utilization and cloud resources consumption, reducing IT expenses. Check out the detailed benchmarking results on the product page. Go to Liberica JDK Performance Edition Page Key Enhancements in the New Version The first Liberica JDK Performance Edition version, which was released in August 2023, was based on JVM 17. This upgrade substitutes JVM 17 with JVM 21, bringing even more performance improvements to legacy Java versions. Which features will you unlock by migration to liberica-perf? They include, but are not limited to: Z Garbage Collector (new in JDK 8, improved in JDK 11), a scalable low-latency GC with generational mode; Improved G1 Garbage Collector; Compact Strings (new in JDK 8), a space-efficient internal representation of Strings; Unified JVM Logging (new in JDK 8); Improved Class Data Sharing (CDS) for faster start. The documentation provides more information on liberica-perf features and added and removed runtime options. Explore liberica-perf Documentation Enjoy the Performance Boost Now and Make the Future Upgrades Easier Liberica JDK Performance Edition 8 lets you upgrade your application to modern performance levels without requiring a Java upgrade. If you already have a Liberica JDK subscription, liberica-perf is included at no extra charge, alongside the rest of our Java tooling. Contact us to request demo builds and try them out with your application. Request Demo Builds - [BellSoft Moves to OSV Format for Transparent Security Disclosure](https://bell-sw.com/blog/bellsoft-moves-to-osv-format-for-transparent-security-disclosure/): We're excited to announce that BellSoft has adopted the Open Source Vulnerabilities (OSV) schema for reporting CVEs in Alpaquita Linux, Stream and LTS versions. Every vulnerability we identify there is now reported in a standardized format that will integrate seamlessly with the security tools developers rely on. This move positions us alongside 25+ technology companies and open source projects in a unified effort to make open source software more secure and transparent for developers worldwide. The Challenge: No Standard Format for Open Source Vulnerabilities Before OSV, there was no existing standard format that could adequately address the unique challenges of open source vulnerability management. The problems were fundamental: Imprecise Version Matching: Existing mechanisms like CPEs (Common Platform Enumerations) couldn't enforce version specifications that precisely match the naming and versioning schemes used in actual open source package ecosystems. Matching a CVE to a specific package name and set of versions in a package manager was nearly impossible to do reliably in an automated way. Ecosystem Fragmentation: No format could describe vulnerabilities across all open source ecosystems without requiring ecosystem-dependent logic to process them. Each language, package manager, and distribution needed its own special handling. Automation Barriers: Existing formats weren't designed to be easily consumed by both automated systems and humans, creating friction in security workflows and slowing down detection and remediation. These limitations meant vulnerability databases, open source users, and security researchers couldn't easily share tooling or consume vulnerability data across the entire open source landscape. The result was fragmented security coverage and slower response times when vulnerabilities were discovered. Enter OSV: The Standard for Vulnerability Data The Open Source Vulnerabilities (OSV) schema emerged from Google's security team around 2021 to address these fundamental problems. Rather than forcing open source projects into rigid, centralized systems, OSV works with the concepts developers actually use daily—git commits, package versions, and ecosystem-specific identifiers. OSV addresses these challenges through three key design principles: Precise Version Matching: OSV enforces version specifications that exactly match the naming and versioning schemes used in actual package ecosystems, eliminating the guesswork in automated vulnerability detection. Universal Ecosystem Support: A single format works across all open source ecosystems without requiring ecosystem-specific processing logic, enabling truly unified tooling. Dual Accessibility: The schema is designed to be equally consumable by automated systems and human reviewers, streamlining both tooling integration and manual analysis. This unified approach means vulnerability databases, open source users, and security researchers can finally share tooling and consume vulnerabilities across all of open source, creating more complete vulnerability coverage and faster detection and remediation times. Industry Adoption: The New Standard OSV has rapidly become the de facto standard for vulnerability data sharing in the open source community. GitHub's Security Advisory Database uses OSV as its foundation. Major language ecosystems including Python, Rust, and Go have embraced the format. Security tools like Dependabot and Snyk consume OSV data directly. Linux distributions haven't been left behind either. Rocky Linux, AlmaLinux, SUSE, Debian, Ubuntu, and others have adopted OSV, recognizing its value for precise vulnerability communication. Popular security scanners like Trivy, Grype, and Syft are built to consume OSV data, ready to provide accurate security assessments when integrated with OSV-compatible sources. BellSoft's OSV Implementation BellSoft has maintained public Security Advisories for all our products, ensuring transparency in vulnerability disclosure. Our OSV adoption for Alpaquita Linux (both Stream and LTS distributions) builds on this foundation by adding machine-readable format support to our existing vulnerability reporting. This enhancement makes analyzing vulnerability applicability to specific installations of our distributions significantly simpler and more transparent for our customers and users. All vulnerability data for Alpaquita Linux (both Stream and LTS distributions) now follows the OSV schema, including kernel-level vulnerabilities and user-space package vulnerabilities that might affect your deployments. You can access this information in two ways: browse our Security Advisories directly on our website, or prepare for future integration with OSV-compatible security scanning tools. Our vulnerability data now lives in the broader OSV ecosystem—a centralized but community-driven database that's not proprietary to any single vendor. When security scanners add support for BellSoft products, they'll follow a two-step process: first, they'll identify that you're using Alpaquita or Liberica in your environment. Second, armed with your exact version and component information, they'll query the OSV database to determine whether specific vulnerabilities are present or have been patched in your deployment. Preparing for Seamless Security Integration This standardization directly addresses the core problems that have plagued open source security. Teams will benefit from precise package matching that eliminates false positives, universal tooling that works across all ecosystems, and automated vulnerability detection that integrates seamlessly into existing workflows. No more manual cross-referencing between incompatible databases or dealing with ecosystem-specific security tools. The standardized format means vulnerability information will reach developers wherever they need it, in tools they already trust and workflows they've already established. Our Commitment to Open Source Security "Security has always been at the heart of everything we build," notes Alexander Belokrylov, BellSoft's co-founder and CEO. "We believe in and love open source, but we also recognize that it can be vulnerable. That's precisely why we do everything possible to make it more transparent and secure. With Alpaquita now supporting the OSV schema, we're preparing for a developer experience that will be significantly easier. Everything they need will be in one place, accessible through tools they already trust." Looking Forward: Building on This Foundation OSV adoption for Alpaquita represents the first step in our enhanced security tooling strategy. We're planning to extend OSV support to Liberica JDK and Liberica NIK in future releases, creating a unified security reporting standard across our entire product portfolio. We're working toward broader integration with security scanners, ensuring that when they add BellSoft product support, the experience will be seamless and reliable. This continues our commitment to making security easy for our users while reinforcing our security-first approach. We don't leave security gaps unaddressed—we identify them, report them transparently through standardized channels, provide clear guidance on remediation, and deliver patches promptly. By adopting OSV, we're not just following an industry trend—we're ensuring that our security efforts align with the tools and workflows our community depends on. It's another step in our ongoing commitment to making open source software more secure for everyone. - [Ultimate Guide to Using DTOs with Spring Boot](https://bell-sw.com/blog/ultimate-guide-to-using-dtos-with-spring-boot/): Sending an entire customer object with a password, credit card number, and national ID to the client sounds like a developer’s nightmare, but in fact, it is a pretty realistic scenario. The most popular approach to avoiding such situations is to use Data Transfer Objects (DTOs). But are DTOs a necessity or overkill? Is there an alternative approach? This article covers the essentials of safely passing data between the server and client. Expect: DTO fundamentals and benefits, A tutorial on using DTOs with records in Spring Boot, A guide to manual vs automated mapping with MapStruct, Additional approaches to retrieving data from the database. Table of Contents What Are DTOs (Data Transfer Objects)? How to Use DTOs with Spring Boot DTOs with Modern Java: Using Records Mapping Entities to DTOs: Manual Mapping DTO Projections Automated Mapping with MapStruct Do We Always Need DTOs? Alternatives to Consider Using @JsonIgnore and @JsonView JPA Entity Graph Recap What Are DTOs (Data Transfer Objects)? Data Transfer Objects (DTOs) are POJOs that serve as data carriers between processes, layers, or services. Their only purpose is to define the shape of the data passed to or from the client. In this sense, entities can also serve as DTOs if you use an alternative approach to creating additional data carrier classes. In this article, I’ll refer to classes with JPA annotations as entities and records without JPA annotations as DTOs. How can DTOs help us in production? They enable the separation of concerns. While entities represent the domain's relational model, DTOs aim to transfer data between system layers. Consequently, they help to avoid the exposure of DB internal details to API consumers. They enhance security. DTOs help developers expose only necessary information, keeping confidential data such as passwords safe. They can improve performance. DTOs can carry data only from necessary fields, helping to avoid serialization and network overhead. How to Use DTOs with Spring Boot This article focuses on using DTOs with Spring Data JPA. The Database Connectivity API — JPA, JDBC, jOOQ, etc. — doesn’t change the concept of DTOs, but it may affect the mapping strategy. Therefore, the article may be replenished with other APIs in the future. To complete this guide, you will need: Java 17 or higher. As it is a Spring Boot demo, I'm using Liberica JDK recommended by Spring. Your favorite IDE. I'm using IntelliJ IDEA. The following dependencies: Spring Web, Spring Data JPA, H2 Database, Validation. The code from this article is available on GitHub. DTOs with Modern Java: Using Records Records represent immutable data carriers. On the surface, they require only the specification of the fields. Under the hood, they make all fields private and final and automatically create accessors, a constructor, equals(), hashCode(), and toString(). This makes the declaration of simple data carriers more concise. DTOs are a good match for records. For instance, let’s look at the following entity class: @Entity @Table(name="employees") public class Employee { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; private String password; // getters, setters, constructors are omitted for brevity } Let’s create the corresponding DTO: public record EmployeeDTO(Long id, String name, String email) { } As you can see, EmployeeDTO contains only three fields and no password. This was an example of a Response DTO. In some cases, if DTO and entity fields match, one DTO may suffice. But what if the data you receive from the client differs from the data you send from the server? In this case, you can create a Request DTO in addition to a Response DTO. The Request DTO for Employee may look like this: public record EmployeeRequestDTO(String name, String email, String password) { } But you can enhance it with validation because records allow us to add annotations to their fields: public record EmployeeRequestDTO(@NotNull String name, @NotNull String email, @NotNull String password) { } What if the entity contains nested entities? Let’s look at the following example, where Customer has a List of Orders, and Order contains Customer: @Entity @Table(name="customers") public class Customer { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; private String password; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name = "customer_id") List orders = new ArrayList<>(); // getters, setters, constructors are omitted for brevity } @Entity @Table(name="orders") public class Order { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "total_price") private double totalPrice; @ManyToOne(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn(name="customer_id") private Customer customer; // getters, setters, constructors are omitted for brevity } In this case, we can use a Composite DTO, where CustomerResponse DTO includes a List of OrderResponse DTOs: public record CustomerResponse(Long id, String name, String email, List orders) { } public record OrderResponse(Long id, double totalPrice) { } Mapping Entities to DTOs: Manual Mapping Manual mapping is a reliable approach to working with DTOs, as you can tailor the mappers exactly as needed. In addition, manual mapping doesn’t require third-party libraries. Hence, no surprises at runtime and a smooth debugging experience. Let’s see how we can implement mappers for our DTOs. A mapper is a regular Java class with primary methods toDto() and toEntity() and possibly additional methods for formatting and computing values. A basic mapper for our Customer model could look like that: public class ManualCustomerMapper { public CustomerResponse mapToCustomerResponse(Customer customer) { return new CustomerResponse( customer.getId(), customer.getName(), customer.getEmail(), customer.getOrders() .stream() .map(this::mapToOrderResponse) .toList()); } public OrderResponse mapToOrderResponse(Order order) { return new OrderResponse(order.getId(), order.getTotalPrice()); } public Customer mapToCustomer(CustomerRequest customerRequest) { Customer customer = new Customer(); customer.setName(customerRequest.name()); customer.setEmail(customerRequest.email()); customer.setPassword(customerRequest.password()); return customer; } } Then, we can add this mapper to CustomerService: @Service public class CustomerService { private final CustomerRepository customerRepository; private final CustomerMapper customerMapper; public CustomerService(CustomerRepository customerRepository, CustomerMapper customerMapper) { this.customerRepository = customerRepository; this.customerMapper = customerMapper; } public List findAll() { return customerRepository .findAll() .stream() .map(customerMapper::mapToCustomerResponse) .toList(); } If the API requires a different format for some data, you can add a value transformation to the mapper. For instance, let’s add an enum to our Customer: @Entity @Table(name="customers") public class Customer { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; private String password; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name = "customer_id") List orders = new ArrayList<>(); private Status status; // getters, setters, constructors, etc. are ommitted for brevity } public enum Status { BRONZE, SILVER, GOLDEN } Also, let’s update the CustomerResponse DTO to include this new field and an additional field, totalExpenses, which represents the sum of all order prices: public record CustomerResponse(Long id, String name, String email, String status, List orders, double totalExpenses) { } The corresponding mapper could be: public CustomerResponse mapToCustomerResponse(Customer customer) { List orderResponses = customer.getOrders() .stream() .filter(Objects::nonNull) .map(this::mapToOrderResponse) .toList(); return new CustomerResponse( customer.getId(), customer.getName(), customer.getEmail(), statusToString(customer.getStatus()), orderResponses, orderResponses .stream() .filter(Objects::nonNull) .map(OrderResponse::totalPrice) .reduce(0.0, Double::sum)); } private String statusToString(Status status) { return status.name(); } When we want to make a request, we usually prefer to reference the object by ID, which is a typical pattern of usage for DTOs. We don’t want to fetch an object from the database when creating a model object to avoid extra SELECTs. So, we can use EntityManager and its method getReference() that returns only the object proxy with only the Id field initialized: public class OrderMapper { private EntityManager entityManager; public Order toOrder(Long customerId, OrderRequest request) { Order order = new Order(); order.setCustomer(entityManager.getReference(Customer.class, customerId)); order.setTotalPrice(request.totalPrice()); return order; } } DTO Projections To avoid overfetching and related N+1 / LazyInitializationException issues when building a DTO, you can use query-time projection to fetch only the required fields. In this case, you get the DTO based on the attribute types returned by a SQL query without the need for a mapper. Let’s look at the following Entity and its DTO: @Entity @Table(name="device") public class Device { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String model; @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn(name="manufacturer_id") private Manufacturer manufacturer; private String serialNumber; private int lotNumber; @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn(name="supplier_id") private Supplier supplier; // getters, setters, constructors, etc. are ommitted for brevity } public record DeviceDTO(String serialNumber, String model) { } We want to return only the serial number and model, leaving behind all other fields, including nested objects. If the DTO field names match exactly the entity field names, you can use a query method: public interface DeviceRepository extends CrudRepository { List findByLotNumber(int lotNumber); } You can also return a Projection DTO using JPQL or Native queries. Here’s the example with a Native query: public interface DeviceRepository extends CrudRepository { @Query("SELECT d.serialNumber, d.model FROM device d WHERE d.lotNumber = :lotNumber") List findByLotNumber(int lotNumber); } You can also use dynamic projections in case you want to decide whether to return an Entity or a DTO at invocation time: public interface DeviceRepository extends CrudRepository { Collection findByLotNumber(int lotNumber, Class type); } Then, in the service layer: void doSomethingMeaningful(int lotNumber) { Collection devices = deviceRepository.findByLotNumber(123456, Device.class); Collection deviceDtos = deviceRepository.findByLotNumber(123456, DeviceDTO.class); } What if we have nested entities and want to include specific fields into our DeviceDTO, such as the name of the manufacturer or a ManufacturerDTO? In case you want to retrieve only some fields from nested entities, you can build a JPQL query: public record DeviceDTO(String serialNumber, String manufacturerName) {} public interface DeviceRepository extends CrudRepository { @Query(""" select new dev.cat.device.dto.DeviceDTO(d.serialNumber, d.manufacturer.name) from Device d where d.lotNumber = :lotNumber """) List findByLotNumber(int lotNumber); } Note that with the JPQL query, we must invoke the DTO constructor using a new keyword and a fully qualified name of the DTO. What if you have nested DTOs? public record DeviceDTO(String serialNumber, ManufacturerDTO manufacturer) { } public record ManufacturerDTO(Long id, String name) { } Most JPA providers, including Hibernate, support nested constructor expressions so that you can write a query for that as well: public interface DeviceRepository extends CrudRepository { @Query(""" select new dev.cat.device.dto.DeviceDTO( d.serialNumber, new dev.cat.device.dto.ManufacturerDTO(m.id, m.name) ) from Device d join d.manufacturer m where d.lotNumber = :lotNumber """) List findByLotNumberNested(int lotNumber); } Automated Mapping with MapStruct If you are fine with doing mapping in the application instead of the database, you can consider using the MapStruct library that automates DTO mapping. MapStruct aims to reduce the boilerplate and error-prone mapping code that developers have to write and generates mappers automatically at compile-time. To use MapStruct, you need to add a MapStruct dependency: org.mapstruct mapstruct 1.5.5.Final And a MapStruct processor to the build plugin: org.apache.maven.plugins maven-compiler-plugin 3.13.0 org.mapstruct mapstruct-processor 1.5.5.Final In a perfect world, if our Employee didn’t have a password so that entity and DTO fields could be the same: @Entity @Table(name="employee") public class Employee { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; // getters, setters, constructors are omitted for brevity } public record EmployeeDTO(Long id, String name, String email) { } You can can create a Mapper interface with the @Mapper annotation and two simple methods: @Mapper public interface EmployeeMapper { EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class); EmployeeDTO mapEmployeeToDto(Employee employee); Employee mapDtoToEmployee(EmployeeDTO dto); } The INSTANCE field can be used when you need to perform the mapping in your Service classes (or wherever you prefer to do the mapping). That’s it! You don’t have to create a Mapper implementation because it will be automatically generated when you run the application. Below is the implementation MapStruct generated for us under target/generated-sources/annotations/dev/cat/EmployeeMapperImpl.java: @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2025-08-10T13:04:45+0300", comments = "version: 1.5.5.Final, compiler: javac, environment: Java 24.0.2 (BellSoft)" ) public class EmployeeMapperImpl implements EmployeeMapper { @Override public EmployeeDTO mapEmployeeToDto(Employee employee) { if ( employee == null ) { return null; } Long id = null; String name = null; String email = null; id = employee.getId(); name = employee.getName(); email = employee.getEmail(); EmployeeDTO employeeDto = new EmployeeDTO( id, name, email ); return employeeDto; } @Override public Employee mapDtoToEmployee(EmployeeDTO dto) { if ( dto == null ) { return null; } Employee employee = new Employee(); employee.setId( dto.id() ); employee.setName( dto.name() ); employee.setEmail( dto.email() ); return employee; } } But our Employee has a password, right? We don’t want to map the password to EmployeeDTO. In this case, we can exclude the missing field from mapping by adding the unmappedTargetPolicy to the @Mapper annotation: @Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) public interface EmployeeMapper { EmployeeMapper INSTANCE = Mappers.getMapper(EmployeeMapper.class); EmployeeDTO mapEmployeeToDto(Employee employee); Employee mapDtoToEmployee(EmployeeDTO dto); As a result, MapStruct will ignore unmapped properties and map only what can be mapped. In addition, it won’t issue any warnings during compilation. How about nested DTOs and calculated values? Can MapStruct handle these use cases? Let’s look at Customer and Order DTOs: public record CustomerResponse(Long id, String name, String email, List orders, double totalExpenses) { } public record OrderResponse(Long id, double totalPrice) { } The OrderMapper is nothing fancy, it’s just the interface like we saw above: @Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE) public interface OrderMapper { OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class); OrderResponse mapToOrderResponse(Order order); } The CustomerMapper is more complex as it needs to perform certain calculations and also, call the OrderMapper instance to map Orders to DTOs. We should make it an abstract class in this case and define our custom logic: @Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = "spring") public abstract class CustomerMapper { public CustomerResponse mapToCustomerResponse(Customer customer) { List orderResponses = customer.getOrders() .stream() .filter(Objects::nonNull) .map(OrderMapper.INSTANCE::mapToOrderResponse) .toList(); double totalExpenses = orderResponses .stream() .filter(Objects::nonNull) .map(OrderResponse::totalPrice) .reduce(0.0, Double::sum); return new CustomerResponse( customer.getId(), customer.getName(), customer.getEmail(), orderResponses, totalExpenses ); } } Note that this class is annotated additionally with componentModel = "spring". This is because we will use this mapper as a regular Spring bean and inject it into CustomerService: @Service public class CustomerService { private final CustomerRepository customerRepository; private final CustomerMapper customerMapper; public CustomerService(CustomerRepository customerRepository, CustomerMapper customerMapper) { this.customerRepository = customerRepository; this.customerMapper = customerMapper; } public List findAll() { return customerRepository .findAll() .stream() .map(customerMapper::mapToCustomerResponse) .toList(); } } That’s it! You mastered the essentials of the Mapstruct library. You can read about other use cases and possible configurations in the documentation. Do We Always Need DTOs? Alternatives to Consider DTOs are a solid approach to separating the data persistence layer from the API. However, when you have a small and simple application, or you are writing a demo to showcase some Sprng features and whatsnot, DTOs may become an unnecessary layer of complexity. In some cases, it is easier to start simple and add DTOs as the business logic becomes more complex. Importantly, having the DTO layer is a de-facto standard of writing enterprise applications, but it is not carved in stone. You must understand when you need to use DTOs and when they are unnecessary. In the latter case, you can rely on Jackson to limit the data visibility to the allowed degree. Here, we’ll look at two solutions Jackson provides to use any class as a DTO: @JsonIgnore and @JsonView annotations. Using @JsonIgnore and @JsonView The @JsonIgnore annotation instructs that a field must be ignored when serializing and deserializing the Entity. For instance, we could annotate the password field of the Student entity like that: @Entity @Table(name="student") public class Student { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; @JsonIgnore private String password; // getters, setters, constructors are omitted for brevity } This way, the resulting JSON will never contain the password when the Entity is serialized. Important information will never be leaked, but instead of several DTOs, we have only our Entity. Let’s consider another situation. What if we don’t want to return the student’s email to the regular users of the API, but want to return it to the admin, for instance? If we used DTOs, we would have to create two more records: one without email and password, and another with email but without password. But actually, we could use the @JsonView annotation. This annotation enables us to define multiple views for the same object. The fields are annotated with one or several view classes, and you select which views to use for serialization. For instance, let’s look at the following Views class with two embedded static classes, Public and Internal: public class Views { public static class Public {} public static class Internal extends Public {} } In the Student class, all the fields annotated with @JsonView(Views.Public.class) should be available to the general audience and internal personnel. On the other hand, fields annotated with @JsonView(Views.Internal.class) should be visible only to internal personnel: @Entity @Table(name="student") public class Student { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) @JsonView(Views.Public.class) private Long id; @JsonView(Views.Public.class) private String name; @JsonView(Views.Internal.class) private String email; @JsonIgnore private String password; // getters, setters, constructors are omitted for brevity } Now, how do we handle these annotations in the Controller? Fields annotated with @JsonIgnore will be silently ignored; no need to update the Controllers. Views require slight code changes, but all you have to do is add the @JsonView annotation to the relevant methods: @GetMapping("/employee") @JsonView(Views.Public.class) public Student getStudent(Long id) { return studentService.findById(id); } @GetMapping("/admin/employee") @JsonView(Views.Internal.class) public Student getStudentInternal(Long id) { return studentService.findById(id); } JPA Entity Graph With Spring Data JPA, there’s always a risk of N+1 and related issues. Developers can choose a fetching strategy for nested entities, FetchType.LAZY or FetchType.EAGER, to tackle such issues. The problem with these fetching strategies is that they are static and cannot be switched at runtime. However, there is a way to implement a per-case-fetch plan thanks to JPA Entity Graph. With JPA Entity Graph, developers let the persistence layer know which associations should be fetched eagerly for a given query. As a result, the JPA provider loads all graphs in one SELECT query and doesn’t use additional SELECT queries for fetching associations, which can result in better performance. You can define the entity graph with annotations or programmatically with the JPA API. We’ll look at annotations in this article. To define one entity graph, we use the @NamedEntityGraph annotation applied at the class level. Note that multiple @NamedEntityGraph annotations can be applied to define several graphs. @NamedEntityGraph is applied to the root entity. Take our Device class, for instance. As it is the root entity used in the queries, we define the named entity graph in the Device class. This annotation allows us to specify the attributes that we want to load for this entity. The attributes are specified with @NamedAttributeNode: @NamedEntityGraph( name = "device-entity-graph", attributeNodes = { @NamedAttributeNode("manufacturer"), @NamedAttributeNode("supplier"), } ) @Entity @Table(name="devices") public class Device { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String model; @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn(name = "manufacturer_id") private Manufacturer manufacturer; private String serialNumber; private int lotNumber; @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn(name = "supplier_id") private Supplier supplier; } Suppose the Manufacturer class has a nested Address entity, and we want to load it as well, when fetching the device with its manufacturer. In this case, we can use the subgraph attribute to load nested associations via the @NamedSubgraph annotation: @NamedEntityGraph( name = "device-entity-graph", attributeNodes = { @NamedAttributeNode(value = "manufacturer", subgraph = "address-subgraph") }, subgraphs = { @NamedSubgraph( name = "address-subgraph", attributeNodes = { @NamedAttributeNode("address") } ) } ) @Entity @Table(name = "device") public class Device { //... } Let’s now see how we can load a graph dynamically. There are two properties, aka hints, that let us specify whether we want to load or fetch the Entity Graph: jakarta.persistence.fetchgraph: attributes specified in the graph will be loaded eagerly, all other attributes will be loaded lazily. In our case, it means that even though the supplier has the eager fetching strategy, it will be loaded lazily anyway. jakarta.persistence.loadgraph: attributes specified in the graph will also be loaded eagerly, but all other attributes will be loaded according to their fetching strategy. So, the supplier will be loaded eagerly as well. public class CustomDeviceRepositoryImpl implements CustomDeviceRepository { private final EntityManager entityManager; public CustomDeviceRepositoryImpl(EntityManager entityManager) { this.entityManager = entityManager; } @Override public Device findByIdWithFetchGraph(Long id) { var entityGraph = entityManager.getEntityGraph("device-entity-graph"); Map properties = new HashMap<>(); properties.put("jakarta.persistence.fetchgraph", entityGraph); return entityManager.find(Device.class, id, properties); } @Override public Device findByIdWithLoadGraph(Long id) { var entityGraph = entityManager.getEntityGraph("device-entity-graph"); Map properties = new HashMap<>(); properties.put("jakarta.persistence.loadgraph", entityGraph); return entityManager.find(Device.class, id, properties); } @Override public Device findByIdWithoutGraph(Long id) { return entityManager.find(Device.class, id); } } Note that if you use the find() method of EntityManager without specifying the graph, all fields will be loaded as per their fetching strategy. So, the manufacturer will be loaded lazily, and the supplier eagerly. Here’s the resulting SQL of fetching devices without the entity graph. As you can see, the manufacturer was lazy loaded, and supplier eagerly loaded as per the defined fetching strategy: SELECT d1_0.id, d1_0.lot_number, d1_0.manufacturer_id, d1_0.model, d1_0.serial_number, s1_0.id, s1_0.name FROM device d1_0 LEFT JOIN supplier s1_0 ON s1_0.id = d1_0.supplier_id WHERE d1_0.id = ?; Here’s the resulting SQL of fetching devices with the fetch graph. The supplier was loaded lazily. The manufacturer was loaded eagerly plus the address: SELECT d1_0.id, d1_0.lot_number, m1_0.id, a1_0.id, a1_0.city, a1_0.country, a1_0.zip, m1_0.license, m1_0.name, d1_0.model, d1_0.serial_number, d1_0.supplier_id FROM device d1_0 LEFT JOIN manufacturer m1_0 ON m1_0.id = d1_0.manufacturer_id LEFT JOIN address a1_0 ON a1_0.id = m1_0.address_id WHERE d1_0.id = ?; Here’s the resulting SQL of fetching devices with the load graph. Both the supplier and the manufacturer were loaded eagerly: SELECT d1_0.id, d1_0.lot_number, m1_0.id, a1_0.id, a1_0.city, a1_0.country, a1_0.zip, m1_0.license, m1_0.name, d1_0.model, d1_0.serial_number, s1_0.id, s1_0.name FROM device d1_0 LEFT JOIN manufacturer m1_0 ON m1_0.id = d1_0.manufacturer_id LEFT JOIN address a1_0 ON a1_0.id = m1_0.address_id LEFT JOIN supplier s1_0 ON s1_0.id = d1_0.supplier_id WHERE d1_0.id = ?; You can use this approach to return stripped down entities or combine it with DTOs. Recap A quick summary? DTOs are a practical way to separate the persistence model from the API contract and keep sensitive data hidden. Java records make DTOs concise and immutable, and mapping strategies include manual mappers and code generators such as MapStruct. On the other hand, using DTOs may lead to having dozens of additional classes you must maintain. You can use Jackson capabilities to trim or shape JSON output directly from entities, such as annotations like @JsonIgnore or @JsonView. In this case, there’s no need to write lots of additional classes for DTOs and Mappers. The downside of using annotations could be hundreds of annotations in your code. In addition, there’s a risk of additional complexity if you want to use entity fields with various views. These annotations should be used with caution and understanding of what you are doing. You can use the JPA Entity Graph in addition to or instead of DTOs to control the fetching strategy at runtime. Learned something new from this article? Great! Don’t forget to subscribe to our newsletter for a monthly digest of our articles and videos on Java development and news in the Java world! - [How To Use Liquibase with Spring Boot](https://bell-sw.com/blog/how-to-use-liquibase-with-spring-boot/): Liquibase is a database migration tool that allows you to track, apply, and roll back changes to the database schema using declarative changelog files. It integrates with version control systems, ensures automatic and reproducible deployment of migrations across different environments, makes the change history transparent, and works consistently with various databases. This article will guide you through setting up and using Liquibase with a Spring Boot application and Maven. You will learn how to: Set up Liquibase for PostgreSQL, Write migration scripts in YAML and SQL, Use preconditions, Perform rollbacks, Use Liquibase diff to compare two versions of a database and create migration scripts automatically, Run database migrations in CI using GitHub Actions. The code from this article is available on GitHub. Table of Contents Liquibase Structure and Commands Changelogs Changesets Change types Changelog tags Liquibase Commands Integrate Liquibase into a Spring Boot Application Write a Changelog File in YAML Write a Changelog File in SQL Use Preconditions in Liquibase Changelogs What are Preconditions? Preconditions Syntax How to Handle Precondition Errors and Failures Perform Rollbacks with Liquibase Generate Changelogs with Liquibase diff Comparing Two Database Versions with Liquibase diff Benefits and Drawbacks of Diffs Use the Liquibase Hibernate Plugin with Maven to Generate Diffs Run Database Migrations in CI/CD with Liquibase and GitHub Actions Conclusion Liquibase Structure and Commands Liquibase enables you to evolve database schemas using data-agnostic formats: YAML, XML, and JSON. These formats are helpful if you need to manage one schema across several databases. However, you can also write scripts in SQL if you prefer. To work with Liquibase, you need to familiarize yourself with the following key concepts. Changelogs Changelog files are the main building blocks of database migrations. They sequentially describe the history of the database changes. You can write a changelog in any supported format, and Liquibase will detect the format by the file extension. It is a good practice to maintain a main changelog that includes links to other changelogs with the actual schema changes. Changesets Changesets are the atomic units of work included in a changelog. For easier rollback, it is a good practice to specify one schema change per changeset. Changesets are applied once and tracked by Liquibase. Change types Change types are database-independent operations specified in a changeset. They correspond to SQL statements applied to the database. Some examples are createTable, createIndex, addColumn, addForeignKeyConstraint, etc. Changelog tags Changelog tags can be described as execution controls. They help to specify when and where a changeset should run. Three types of changelog tags are: Preconditions that help to perform checks against the current database state. For instance, “table X must exist.” If the check fails, Liquibase can halt, skip, or mark the changeset as ran as instructed by you. Context tags are boolean flags you attach to changesets, e.g., dev, staging. You pass a --context-filter at runtime, and only matching changesets execute. Labels are similar to context tags but are added to changesets as labels and specified at runtime with a --label-filteroption. Liquibase Commands Liquibase supports ~40 commands that can be classified into six categories: Init to bootstrap Liquibase to a project. Update to apply any pending changes to the target database. Rollback to undo changes by count, tag, or date. Inspection to compare sources and generate diffs. Change tracking to get the deployment status of changes. Utility to manage schema documents and tasks. Integrate Liquibase into a Spring Boot Application Prerequisites: Java 17 or higher. As we are working with Spring Boot, I use Liberica JDK 24 recommended by Spring. Local PostgreSQL Server or Docker Compose if you want to spin up a database in a container. This demo uses the second option. Your favorite IDE. I’m going to use a cyberpunk-themed demo project called Neurowatch. You can clone the project from GitHub to follow along, use your own app, or create a basic Spring Boot app. If you use your own project, add the following dependency on Liquibase: org.liquibase liquibase-core In the application.properties file, disable Hibernate DDL, enable Liquibase and specify the path to the main changelog file: # Hibernate is disabled because Liquibase will manage schema spring.jpa.hibernate.ddl-auto=none spring.jpa.show-sql=true # Liquibase spring.liquibase.enabled=true spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml If you added Liquibase when creating the project, Spring created db/changelog directories for you under resources. Otherwise, you need to create them manually. In the db/changelog directory, create a db.changelog-master.yaml file with the following content: databaseChangeLog: - include: file: /src/main/resources/db/changelog/001-create-tables.yaml The 001-create-tables.yaml will be our first changelog. Other changelogs will be added to the main changelog in a similar fashion. We’re all set. It's time to write some migrations! Write a Changelog File in YAML The first changelog will describe the initial database schema. The file starts with a top-level key pointing out that this file is a changelog: databaseChangeLog: After that, you add a changeSet entry and specify its id, author, any other required info, and preconditions if any: databaseChangeLog: - changeSet: id: 001 author: cyberjar preConditions: Then, you begin the list of actual operations, a.k.a. the change types. In this case, we will have two change types, addColumn and addForeignKeyConstraint. Everything indented under the line with the change type configures that operation: databaseChangeLog: - changeSet: id: 001 author: cyberjar changes: - createTable: The whole file: databaseChangeLog: - changeSet: id: 001 author: cyberjar changes: - createTable: tableName: civilian columns: - column: name: id type: BIGINT autoIncrement: true constraints: primaryKey: true - column: name: legal_name type: VARCHAR(250) - column: name: national_id type: VARCHAR(50) - column: name: birth_date type: DATE - column: name: criminal_record type: BOOLEAN - column: name: under_surveillance type: BOOLEAN - createTable: tableName: cyberware columns: - column: name: id type: BIGINT autoIncrement: true constraints: primaryKey: true - column: name: name type: VARCHAR(200) - column: name: type type: VARCHAR(50) - column: name: version type: VARCHAR(50) - createTable: tableName: implant_session columns: - column: name: id type: BIGINT autoIncrement: true constraints: primaryKey: true - column: name: civilian_id type: BIGINT - column: name: cyberware_id type: BIGINT - column: name: installed_at type: DATE - column: name: installed_by type: VARCHAR(255) - addForeignKeyConstraint: baseTableName: implant_session baseColumnNames: civilian_id referencedTableName: civilian referencedColumnNames: id constraintName: fk_civilian - addForeignKeyConstraint: baseTableName: implant_session baseColumnNames: cyberware_id referencedTableName: cyberware referencedColumnNames: id constraintName: fk_cyberware Let’s create another changelog 002-insert-sample-data.yaml to populate the DB with test data. Don’t forget to reference this file in the main changelog: databaseChangeLog: - include: file: /src/main/resources/db/changelog/001-create-tables.yaml - include: file: /src/main/resources/db/changelog/002-insert-sample-data.yaml In this case, we will use the change type “insert.” The whole file: databaseChangeLog: - changeSet: id: 002 author: cyberjar changes: - insert: tableName: civilian columns: - column: { name: legal_name, value: "Aelita Fang" } - column: { name: national_id, value: "IZ-0965437-BM" } - column: { name: birth_date, value: "1999-08-30" } - column: { name: criminal_record, value: "FALSE" } - column: { name: under_surveillance, value: "FALSE" } - insert: tableName: cyberware columns: - column: { name: name, value: "OptiSight X3" } - column: { name: type, value: "ocular" } - column: { name: version, value: "SZ 3000" } - insert: tableName: implant_session columns: - column: { name: civilian_id, valueNumeric: 1 } - column: { name: cyberware_id, valueNumeric: 1 } - column: { name: installed_at, valueDate: "2025-06-15" } - column: { name: installed_by, value: "MP-129854" } Now, run the application. You should see in the console that Liquibase has successfully run two changesets. If you open your favorite database tool and connect to the database, you should see that the tables were created and populated. DB schema You will also see two additional tables that Liquibase creates automatically upon the first run: databasechangelog and databasechangeloglock. Write a Changelog File in SQL If you work with one database, you don’t have to use database-agnostic formats and can write changelogs in SQL. The file almost doesn’t differ from the schema docs you are used to writing, except for the formatted comments. The first non-empty line should be --liquibase formatted sql This header that tells Liquibase to parse this file as a “formatted SQL” changelog, not just raw SQL. This way, Liquibase reads and recognizes special comment directives like --changeset author:id, --rollback, --preconditions, --context, --labels, etc. As for the changes, they are described in the Postgres-specific SQL, so nothing new here: --liquibase formatted sql --changeset cyberjar:001 CREATE TABLE civilian ( id BIGINT PRIMARY KEY, legal_name VARCHAR(250), national_id VARCHAR(50), birth_date DATE, criminal_record BOOLEAN, under_surveillance BOOLEAN ); CREATE TABLE cyberware ( id BIGINT PRIMARY KEY, name VARCHAR(200), type VARCHAR(50), version VARCHAR(50) ); CREATE TABLE implant_session ( id BIGINT PRIMARY KEY, civilian_id BIGINT, cyberware_id BIGINT, installed_at DATE, installed_by VARCHAR(255) ); ALTER TABLE implant_session ADD CONSTRAINT fk_civilian FOREIGN KEY (civilian_id) REFERENCES civilian(id); ALTER TABLE implant_session ADD CONSTRAINT fk_cyberware FOREIGN KEY (cyberware_id) REFERENCES cyberware(id); Use Preconditions in Liquibase Changelogs What are Preconditions? Preconditions are special tags that enable you to specify requirements for updating the database. For instance, you can check that changes are applied to a specific database or by a particular user. Preconditions can be applied to the whole changelog or to individual changesets. With XML, JSON, and YAML you can use all preconditions supported by Liquibase. The SQL-based changelogs support only some of them. Preconditions Syntax It is possible to use one preCondition tag per changelog/changeset, but you can specify several requirements using the AND, OR, NOT tags. If no tag is specified, the default AND tag is used under the hood. For instance, in the following changelog, we verify that the database type is Postgres and the user applying changes is dbadmin: databaseChangeLog: - changeSet: id: 001 author: cyberjar preConditions: - dbms: type: postgresql - runningAs: username: dbadmin changes: In the following changelog, the database type should be PostgreSQL or MySQL: databaseChangeLog: - changeSet: id: 001 author: cyberjar preConditions: - or: - dbms: type: postgresql - dbms: type: mysql changes: You can find the whole list of available preConditions in the documentation. This is all very well, but how can we handle the unsatisfactory check results? How to Handle Precondition Errors and Failures There are two types of preconditions: Failures (the onFail attribute) mean that the check has failed. For instance, a column exists when it mustn’t. Errors (the onError attribute) are exceptions thrown during the execution of the check. You can use the required attribute and specify what to do in case of failure or error using the following values: CONTINUE: Liquibase will not execute this changeset, but it will continue with the changelog and try to execute this changeset during the next run. HALT: Liquibase stops the execution of the whole changelog. MARK_RAN: Liquibase will not execute this changeset, but will mark it as executed. WARN: Liquibase will continue executing the changeset/changelog, but will issue a warning. Note that the CONTINUE and MARK_RAN can only be applied to a changeset. For instance, the changeset below includes a precondition that verifies the column doesn’t exist. If the check fails, this changeset will not be applied, but marked as run, meaning that Liquibase won’t attempt to execute it during the next update: databaseChangeLog: - changeSet: id: 004 author: cyberjar preConditions: - onFail: MARK_RAN - not: columnExists: tableName: cyberware columnName: type changes: - addColumn: tableName: cyberware columns: - column: name: type type: VARCHAR(50) Perform Rollbacks with Liquibase Suppose something went wrong and the changes you introduced to the database brought some issues. In this case, you can use the Liquibase rollback functionality supported in CLI and Maven. The rollback script returns the database to a specified point. There are three types of rollback: A simple rollback performed with the rollback command reverts changes to the database after a specified tag. A rollback to some period with the rollback-to-date command reverts changes from the current date to a specified date and time. A rollback by a number of changesets with the rollback-count command rolls back a specified number of changesets. However, writing rollback scripts is difficult, and using rollbacks is associated with risks. Therefore, you should use this feature cautiously and always validate potential changes before they are applied. You can do that with the help of these commands: update-testing-rollback tests the rollbacks by deploying pending changesets, rolling them back, and running the update again. future-rollback-sql generates a SQL that will be used to perform the rollback. Let’s see how we can perform a rollback using Maven. For that, we need to add a new plugin, liquibase-maven-plugin and configure the database connection and the path to the main changelog there: org.liquibase liquibase-maven-plugin 4.27.0 src/main/resources/db/changelog/db.changelog-master.yaml org.postgresql.Driver jdbc:postgresql://localhost:5432/neurowatch neurowatch_user mypassword Now, let’s create a new rollback changelog where we will add a new column to the civilian table. The rollback will drop this column: databaseChangeLog: - changeSet: id: 005 author: cyberjar preConditions: - dbms: type: postgresql - not: columnExists: tableName: civilian columnName: temp_notes changes: - addColumn: tableName: civilian columns: - column: name: temp_notes type: VARCHAR(255) defaultValue: 'N/A' rollback: - dropColumn: columnName: temp_notes tableName: civilian Run the application. The new column should have appeared in the civilian table. Now, let’s perform the rollback. Run the mvn:liquibase command specifying the number of changesets you want to roll back with rollbackCount: mvn liquibase:rollback -Dliquibase.rollbackCount=1 Run the application again. In the console, you will see that the rollback was applied. Verify in your database tool that the column indeed disappeared. Generate Changelogs with Liquibase diff Comparing Two Database Versions with Liquibase diff The Liquibase diff command allows you to compare two database schema versions, DB to DB or DB to JPA via Hibernate. When you run this command, Liquibase analyzes one or two database states and automatically generates a changelog with the differences. The diff command can be very useful in some situations but harmful in others, so it should not be used for everything. Let’s briefly discuss when it is a sagacious advisor and when it is a little drunk elf that desperately wants to help but has no idea what it is doing. Benefits and Drawbacks of Diffs The Liquibase diff command can be helpful when You want to integrate Liquibase into an existing project. In this case, the diff command will compare the existing and empty databases and generate changesets describing how to recreate the current state of the database. The diff command can help you detect changes that weren’t documented. Although the best practice for developers is to introduce all changes through migrations rather than directly to the database, this command can still be used for a quick sanity check. On the other hand, there are some caveats with diffs that you must take into consideration: The diffs show syntactical differences but not semantical ones. For example, if a column was renamed, the generated changelog will tell you that one column should be dropped and another added, leading to data loss. The diffs can’t track the data changes efficiently because they can’t handle the expected differences between environments. Therefore, this command should be used with caution and a complete understanding of what and why one is doing. Always validate the generated changelogs before applying them and adjust them manually if needed. You can also use additional tools, such as JPA Buddy, to validate the potential changes. Use the Liquibase Hibernate Plugin with Maven to Generate Diffs Suppose you reworked your JPA entities a bit and want to generate a changelog describing all the changes you have made. Let’s first add the dependency to Liquibase Hibernate that will enable you to generate the changelog file comparing the current database schema and JPA entities: org.liquibase.ext liquibase-hibernate6 4.27.0 Next, we need to add some additional configurations to the Liquibase Maven plugin: diffChangeLogFile points to the changelog file that will be generated as a result of the database comparison. The name of the file contains a timestamp for easier version tracking. referenceURL pointing to the package in the application where our JPA entities reside. The dialect parameter is required because the referenceUrl performs the package scanning. Hibernate Physical Naming Strategy and Hibernate Implicit Naming Strategy help avoid API incompatibilities. org.liquibase liquibase-maven-plugin 4.27.0 src/main/resources/db/changelog/db.changelog-master.yaml src/main/resources/db/changelog/${maven.build.timestamp}_changelog.yaml org.postgresql.Driver jdbc:postgresql://localhost:5432/neurowatch neurowatch_user mypassword hibernate:spring:dev.cyberjar?dialect=org.hibernate.dialect.PostgreSQLDialect &hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy &hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy Finally, we can run the liquibase:diff command to generate a changelog. Note that this command uses compiled classes from the target directory for comparison, so we need to rebuild the project: mvn clean install liquibase:diff As a result, the changelog will be generated in the db/changelog directory. Run Database Migrations in CI/CD with Liquibase and GitHub Actions Let’s discuss this pipeline step by step. The workflow YAML is configured to run whenever changes are pushed to the main branch of the application. It defines one job, “Verify and Migrate DB with Liquibase”: name: Liquibase CI Pipeline on: workflow_dispatch: push: branches: - main jobs: liquibase-verify: name: Verify and Migrate DB with Liquibase Before conducting any steps within the job, the workflow declares the services it needs to spin up, in this case, a PostgreSQL service. The database user and password are specified explicitly in the example, but in a real setup, you should store credentials as GitHub Secrets and reference them in the password fields. The service also exposes the required ports and includes health-check options, so the job continues only after the database reports itself as ready. runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_DB: neurowatch POSTGRES_USER: neurowatch_user POSTGRES_PASSWORD: mypassword ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 The steps begin by checking out the repository and running a resource processing phase with Maven as a precaution. steps: - name: Checkout repository uses: actions/checkout@v4 - name: Process Resources shell: bash run: | ./mvnw process-resources The interesting part starts with validation. Here, the workflow uses the Liquibase GitHub Action to validate the change sets. It specifies the path to the main changelog file along with the URL, username, and password needed to reach the database. Note that the URL does not point to localhost. Instead, it targets the service name we brought up, postgres. - name: Validate changesets uses: liquibase-github-actions/validate@v4.27.0 with: changelogFile: src/main/resources/db/changelog/db.changelog-master.yaml url: "jdbc:postgresql://postgres:5432/neurowatch" username: neurowatch_user password: mypassword headless: true logLevel: INFO Next comes Preview Update SQL, a dry run of the SQL that would be executed. It uses the update Liquibase GitHub Action and repeats the same configuration details: the changelog file, the database URL, and the credentials. - name: Preview updateSQL (dry run) uses: liquibase-github-actions/update@v4.27.0 with: changelogFile: src/main/resources/db/changelog/db.changelog-master.yaml url: "jdbc:postgresql://postgres:5432/neurowatch" username: neurowatch_user password: mypassword headless: true logLevel: INFO After that, the pipeline performs a Preview Rollback SQL step to verify that rollbacks behave correctly. Here, it uses a different Liquibase GitHub Action for rollbacks, the rollback count SQL in this example, but you can choose the rollback action that best fits your needs. The step includes the rollback count or how many changesets to roll back, and again, passes the username, password, and other required parameters. - name: Preview rollbackSQL (dry run of last changeSet) uses: liquibase-github-actions/rollback-count-sql@v4.27.0 with: changelogFile: src/main/resources/db/changelog/db.changelog-master.yaml count: 1 url: "jdbc:postgresql://postgres:5432/neurowatch" username: neurowatch_user password: mypassword headless: true logLevel: INFO If all those jobs succeed, the pipeline applies the Liquibase migration for real using the update job, with the same set of parameters as before. - name: Apply Liquibase migration uses: liquibase-github-actions/update@v4.27.0 with: changelogFile: src/main/resources/db/changelog/db.changelog-master.yaml url: "jdbc:postgresql://postgres:5432/neurowatch" username: neurowatch_user password: mypassword headless: true logLevel: INFO Once migrations are applied, the workflow runs the tests. If everything passes, the final stage deploys the application. In my demo, it is represented by a simple placeholder command, but in a real pipeline, you would replace it with your actual deployment process. - name: Run tests run: ./mvnw test - name: Deploy app if: success() run: echo "Deploy your app here..." That’s it! The whole file can be found on GitHub. Conclusion In this article, we examined using Liquibase with Spring Boot and Maven for reliable database migrations both locally and in the CI/CD pipeline. Don’t forget to subscribe to our newsletter for a monthly digest of our articles and videos on Java development and news in the Java world! - [Liberica JDK 25 LTS Release: A Long-Term Foundation for Java Applications](https://bell-sw.com/blog/liberica-jdk-25-lts-release-a-long-term-foundation-for-java-applications/): We are happy to announce the general availability of Liberica JDK 25, a new LTS JDK release that will receive quarterly updates and support until September 2033. Download Liberica JDK 25 Long-term supported JDK versions often become the foundation for enterprise applications for many years to come, so if you are planning to upgrade, here’s what you need to know about JDK 25. What’s New in JDK 25 The new LTS release JDK 25 includes 18 JEPs that bring several production-ready features to the Java platform and multiple enhancements aimed at increasing the Java performance, security, and usability. We looked into each JEP in our video “Unboxing Java 25 features” available on YouTube. In this article, we’ll summarize each JEP at a glance. Performance JEP 521: Generational Shenandoah turns the generational mode of Shenandoah GC into a production-ready feature. JEP 519: Compact Object Headers reduces the size of Java object headers to 64 bits on 64-bit architectures. JEP 515: Ahead-of-Time Method Profiling stores method-execution profiles in the AOT cache so the JIT can start optimizing faster on startup. JEP 502: Stable Values (Preview) introduces a new API for objects with deferred initialization holding immutable data. JEP 508: Vector API (Tenth Incubator) keeps refining the API for expressing multiple vector computations that compile to optimal CPU instructions for tangible performance gains. Observability JEP 520: JFR Method Timing & Tracing enables gathering precise per-method statistics via JVM-side bytecode instrumentation. JEP 518: JFR Cooperative Sampling makes JFR stack sampling safer by sampling stacks at safepoints with cooperative requests with reduced bias. JEP 509: JFR CPU-Time Profiling (Experimental) adds an integrated CPU-time profiler to JFR to attribute CPU usage to threads/methods with low overhead. Security JEP 470: PEM Encodings of Cryptographic Objects (Preview) adds an API to encode/decode keys, certs, and certificate revocation lists to/from PEM. JEP 510: Key Derivation Function API standardizes an API for deriving cryptographic keys from passwords/inputs, unifying KDF usage. Usability JEP 505: Structured Concurrency (Fifth Preview) further refines an API for managing groups of related subtasks as a single unit of work. JEP 514: Ahead-of-Time Command-Line Ergonomics merges two commands for creating the AOT cache into one with -XX:AOTCacheOutput. JEP 506: Scoped Values provide a safer alternative to ThreadLocal variables for sharing immutable data down a call tree and to child threads, especially with virtual threads. JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview) extends pattern matching to primitives so instanceof/switch work more uniformly across all types. JEP 511: Module Import Declarations introduces the ability to import all module packages by importing their module, smoothing out program ergonomics. JEP 512: Compact Source Files & Instance Main Methods finalizes the beginner-friendly possibility to write simple Java programs without hardcore enterprise Java features. JEP 513: Flexible Constructor Bodies allows statements before super()/this(), enabling validation and initialization in a constructor. JEP 503: Remove Source & Build Support for 32-bit x86 deserves a separate note. It has served its purpose for many years, but no new 32-bit-only x86 hardware is manufactured anymore. Therefore, it was decided to remove the source code and build support for 32-bit x86 port with the intent of decreasing maintainability burden for new Java features. Should You Consider Changing the JDK Vendor? Upgrading the JDK version means substituting every Java runtime in your enterprise with a more performant, secure, and developer-friendly one. This is a perfect moment to revisit your JDK vendor as well, since costs, licensing, support model, and additional offerings may affect your business strategy even more than the actual Java version. If you use Oracle Java, you may want to consider moving to OpenJDK for two major reasons: Oracle’s Java SE Universal Subscription makes licensing tied to a total number of employees, including part-time workers and temporary personnel. OpenJDK distributions that offer commercial support have more cost-efficient offerings. Free quarterly updates for LTS Oracle Java versions are provided for three years, whereas most OpenJDK vendors provide free updates for a longer period of time. If you doubt the quality of OpenJDK distributions as compared to Oracle Java, read the article comparing Oracle to OpenJDK to get a more detailed picture of their differences and similarities. Get the Most Complete Java Experience with Liberica JDK Several OpenJDK distributions are available: they are all open source and freely available, have the same code base as Oracle Java, but differ in additional offerings. Therefore, the choice should be based on your business needs. Liberica JDK is the only runtime that provides the most complete Java experience: The broadest range of supported platforms and versions. BellSoft supports all LTS versions, the current non-LTS version, legacy Java 6 & 7, and GraalVM. In addition, we provide a Liberica JDK bundle with CRaC support. Three flavors. Liberica JDK Standard, Liberica JDK Full with JavaFX, and Liberica JDK Lite optimized for cloud deployments. Affordable prices. You can use Liberica JDK for free or with our support. Flexible plans are customized for your requirements, and 24/7 service comes directly from Java engineers. Alpaquita Containers based on lightweight Alpaquita Linux tailored for Java applications. Find out even more Liberica JDK features in our overview of Oracle Java alternatives or get a comparative table of commercial offerings of OpenJDK vendors vs Oracle. Download Liberica JDK 25 Now! Upgrading to JDK 25 is the perfect time to rethink not just your version, but your JDK vendor. Liberica JDK gives you a secure, TCK-verified, drop-in replacement with long-term support, performance optimizations, and zero-surprise licensing. Don’t let Java upgrades become a budgeting nightmare, take control of your stack. Download Liberica JDK today and future-proof your Java workloads. Download Liberica JDK - [Liberica Native Image Kit 25.0.0+1 is Available](https://bell-sw.com/blog/liberica-native-image-kit-25-0-0-1-is-available/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) version 25.0.0+1 based on JDK 25+37. This major release brings significant enhancements in performance, security, and developer experience, building upon the solid foundation of the new LTS release JDK 25. Liberica NIK releases are aligned with GraalVM release schedule. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues, including over 1,219 fixes in this release (not counting JDK fixes). Oracle recently announced changes to their GraalVM distribution strategy just days before this release. For Oracle customers: Oracle GraalVM for JDK 24 was the final version licensed and supported as part of Oracle Java SE products. What's New in JDK 25 Since many of the new features in Native Image Kit 25 are aligned with JDK 25 improvements, we recommend checking out our comprehensive Liberica JDK 25 LTS release article for detailed coverage of the 18 JEPs that bring production-ready features to the Java platform. Key JDK 25 highlights that enhance Native Image Kit include: Performance improvements: JEP 519: Compact Object Headers, JEP 515: Ahead-of-Time Method Profiling, and enhanced JEP 508: Vector API (Tenth Incubator) support Enhanced observability: JFR improvements with JEP 520: Method Timing & Tracing, JEP 518: Cooperative Sampling, and JEP 509: CPU-Time Profiling (Experimental) Security enhancements: JEP 470: PEM Encodings of Cryptographic Objects (Preview) and JEP 510: Key Derivation Function API Developer experience: JEP 513: Flexible Constructor Bodies, JEP 511: Module Import Declarations, and improved JEP 505: Structured Concurrency (Fifth Preview) Key Native Image Enhancements Major Performance Improvements Enhanced Vector API optimization: Initial optimization support for JEP 338: Vector API operations in native images, with load, store, arithmetic, reduce, compare, and blend operations transformed to efficient machine instructions. Enable with --add-modules jdk.incubator.vector and -H:+VectorAPISupport. Whole-Program Sparse Conditional Constant Propagation (WP-SCCP): Now enabled by default, improving points-to analysis precision and potentially reducing native binary size. Learn more about this optimization in the SkipFlow article. Enhanced Foreign Function & Memory API Support Extended platform compatibility: JEP 454: Foreign Function & Memory API support is now available on macOS AArch64 and Linux AArch64, with new configuration syntax, Arena.ofShared() implementation, and automatic FFM configuration generation in reachability-metadata.json. See the comprehensive reference documentation for details. New Experimental Tracing Agent Native Image Tracing Agent: Unlike the existing JVM-based tracing agent, this new agent operates in Native Image mode and is more closely aligned with Native Image semantics. Build with -H:Preserve=all and run with -XX:TraceMetadata=path= to generate reachability metadata for the actual types and resources required by Native Image. Simplified Metadata Configuration Advanced metadata handling: The new -H:Preserve option allows you to keep entire packages, modules, or classes on the classpath, even if not discovered by static analysis. JNI registration is now unified in the "reflection" section of reachability-metadata.json, and lambda classes can be registered for reflection and serialization. See the example application and metadata format documentation for more details. Additional Improvements Language runtime updates: ECMAScript 2025 mode enabled by default in GraalJS with Node.js 22.17.1, Python 3.12.8 support in GraalPy with enhanced Windows REPL, improved WebAssembly SIMD proposal support. For a complete list of all enhancements, performance improvements, and technical details, see the full GraalVM Community Edition 25 release notes. Download the New Builds Now! BellSoft continues to provide Java developers with a comprehensive stack of secure and affordable technologies for creating diverse applications. This release represents a significant step forward in native image compilation capabilities, offering: Superior performance through advanced optimization techniques Enhanced security with obfuscation and embedded SBOM support Simplified development with better configuration tools and error reporting Broader platform support with extended FFM API compatibility Download Liberica Native Image Kit 25.0.0+1 today and experience the next generation of Java native compilation technology! - [How to Use Flyway with Spring Boot](https://bell-sw.com/blog/how-to-use-flyway-with-spring-boot/): Flyway is a database migration tool for managing database schema changes using changelog files written in SQL. This article will guide you through setting up and using Flyway with Spring Boot and Maven. You will learn how to: Set up Flyway for PostgreSQL, Perform various migration types, Use conditional statements, Run Flyway from CLI, Apply database migrations in CI with GitHub Actions. The code from this tutorial is available on GitHub. Table of Contents Key Flyway Concepts and Features Integrate Flyway into a Spring Boot Application Write Migration Files Versioned Migrations Repeatable Migrations Baseline Migrations Undo Migrations (Teams Edition) Executing Migrations Conditionally Using Flyway from CLI Running Database Migrations in CI with Flyway and GitHub Actions Conclusion Key Flyway Concepts and Features Flyway is a straightforward, SQL-first solution for keeping relational DB schemas in sync across various environments. You can use it to run migrations at application start or work with it from CLI, a Docker container, or in CI. You write migration scripts in SQL specifying the changes you want to apply. Flyway connects to the target database and runs the scripts in a strict order once, records their checksums in a special table, and fails if something is adrift: for instance, if the previously run script was modified. What makes Flyway stand out as a DB versioning tool? Some of its essential features include: Straightforward versioning model with forward-only migrations (rollbacks are offered as a commercial feature) written in familiar SQL; Java-based migrations for describing changes not easily expressed in SQL; Different types of migrations: versioned, repeatable, baseline, undo. We will look at them in more detail further on; Support for conditional statements, placeholders, and DO Postgres files to specify the conditions that must be fulfilled before the migration is run. Let’s see how we can implement these powerful Flyway features in practice with Spring Boot! Integrate Flyway into a Spring Boot Application Prerequisites: Java 17 or higher. As we are working with Spring Boot, I use Liberica JDK recommended by Spring. Local PostgreSQL Server or Docker Compose if you want to spin up a database in a container. This demo uses the second option. Your favorite IDE. For this tutorial, I’m going to use a cyberpunk-themed demo project called Neurowatch. You can clone the project from GitHub to follow along, use your own app, or create a basic Spring Boot app. First, we need to add the following dependencies on Flyway to the project: org.flywaydb flyway-core org.flywaydb flyway-database-postgresql In the application.properties file, we need to specify a URL, user name, password, and the driver for Postgres SQL. We also need to disable Hibernate because Flyway will manage the schema. Last but not least, enable Flyway and specify the path to the directory where you will store the database migration files: spring.datasource.url=jdbc:postgresql://localhost:5432/neurowatch-flyway spring.datasource.username=neurowatch_user spring.datasource.password=mypassword spring.datasource.driver-class-name=org.postgresql.Driver # Hibernate is disabled because Flyway will manage schema spring.jpa.hibernate.ddl-auto=none spring.jpa.show-sql=true # Flyway spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration All set, it’s time to write some migrations! Write Migration Files As mentioned above, Flyway supports four types of migrations: versioned, repeatable, baseline and undo. Let’s look at all of them. Versioned Migrations Versioned migrations are used for ordered schema changes like adding or removing tables, creating indexes, setting constraints, etc. They are applied exactly once in a strict version order. When you run the migrations for the first time, Flyway creates a flyway_schema_history table, where it records each run migration with a checksum. If the migration file that was already run gets edited, the migration fails. All Flyway migration files must follow a certain naming convention. The naming convention for versioned migrations is V__.sql. For example, V3__add_email_column.sql. Versions can be dotted or underscored like 1.2.0, 001_002. Let’s start with writing our first migration. Create a file V1__create_tables.sql under the resources/db/migration directory and populate it with the familiar Postgres-specific SQL for creating a database schema: CREATE TABLE civilian ( id BIGINT PRIMARY KEY, legal_name VARCHAR(250), national_id VARCHAR(50), birth_date DATE, criminal_record BOOLEAN, under_surveillance BOOLEAN ); CREATE TABLE cyberware ( id BIGINT PRIMARY KEY, name VARCHAR(100), type VARCHAR(50), version VARCHAR(50) ); CREATE TABLE implant_session ( id BIGINT PRIMARY KEY, civilian_id BIGINT, cyberware_id BIGINT, installed_at DATE, installed_by VARCHAR(250), CONSTRAINT fk_civilian FOREIGN KEY (civilian_id) REFERENCES civilian(id), CONSTRAINT fk_cyberware FOREIGN KEY (cyberware_id) REFERENCES cyberware(id) ); Now, let’s create one more migration file called V2__seed_sample_data.sql for populating the database with test data: INSERT INTO civilian (id, legal_name, national_id, birth_date, criminal_record, under_surveillance) VALUES (1, 'Aelita Fang', 'IZ-0965437-BM', '1999-08-30', FALSE, FALSE); INSERT INTO cyberware (id, name, type, version) VALUES (1, 'OptiSight X3', 'ocular', 'SZ 3000'); INSERT INTO implant_session (id, civilian_id, cyberware_id, installed_at, installed_by) VALUES (1, 1, 1, '2025-06-15', 'MP-129854'); Start the PostgreSQL instance and run the application. The migrations will be applied. Open your favorite database migration tool and verify that the tables were indeed created and populated. Whenever you update your entities, you should create a new migration file describing the changes so that Flyway could update the schema accordingly. For example, we added new columns, email to Civilian and manufacturer to Cyberware. Now, let’s create a V3__add_extra_columns.sql file with a following content: ALTER TABLE civilian ADD COLUMN email VARCHAR(255); ALTER TABLE cyberware ADD COLUMN manufacturer VARCHAR(100); A good practice is to consider versioned migrations immutable. Don’t ever change them, create a new migration instead specifying required changes. Repeatable Migrations Repeatable migrations include the definitions you want to re-apply every time there are some changes. For example, you can use them to create or update views procedures or to perform bulk reloads of reference data. Repeatable migrations are executed after all pending versioned migrations. A repeatable migration runs again whenever its checksum changes, i.e., the file content changes. The naming convention is R__.sql (no version). Let’s create a repeatable migration called R__implant_summary_view.sql to get the statistics on all implants when we run the application. For that purpose, the migration will recreate a view joining three tables. You can add a comment with -- to specify what this migration does: -- Repeatable migration: (re)create a view joining all three tables. CREATE OR REPLACE VIEW v_implant_summary AS SELECT s.id AS session_id, c.legal_name, c.national_id, c.birth_date, c.criminal_record, c.under_surveillance, w.name AS cyberware_name, w.type AS cyberware_type, w.version AS cyberware_version, s.installed_at, s.installed_by FROM implant_session s JOIN civilian c ON c.id = s.civilian_id JOIN cyberware w ON w.id = s.cyberware_id; We also need to add a placeholder with a timestamp. Flyway placeholders are text variables you define in configs and reference in migration files. Before a script is sent to the database, Flyway does a string substitution: it finds ${name} and replaces it with a value. Add the following line to the top of the repeatable migration file: -- ${build_timestamp} Next, specify the timestamp variable in application.properties: spring.flyway.placeholders.build_timestamp=${BUILD_TS:dev} This way, the migration will run every time because the timestamp placeholder changes the checksum. So, Flyway will recreate this view and update the statistics on every migration run. Baseline Migrations Baseline migration is a single migration that represents the state of the database after all of the version migrations have been applied. They are useful for rapidly bootstrapping new environments by snapshotting the schema state at a point in time so there’s no need to apply a long chain of V files. The naming convention is B__.sql. For example, B5__snapshot_after_V5.sql represents the state up to and including V5. If you use flyway on a new database, it picks up the latest baseline file, marks every versioned migration which is lower than this baseline as ignored, and starts from the baseline snapshot of the database. However, if the database already has the flyway_history table, baseline files are skipped. Note that baseline migrations do not conflict with the future V migrations, they simply speed up the installation of the database. Let’s add a baseline migration to snapshot the database. Create a file called B4__baseline_after_schema_updates.sql. Here, we create the database schema, but this time, with all the changes we have introduced: -- Snapshot after V3 CREATE TABLE IF NOT EXISTS civilian ( id BIGINT PRIMARY KEY, legal_name VARCHAR(250), national_id VARCHAR(50), birth_date DATE, email VARCHAR(255), criminal_record BOOLEAN, under_surveillance BOOLEAN ); CREATE TABLE IF NOT EXISTS cyberware ( id BIGINT PRIMARY KEY, name VARCHAR(100), type VARCHAR(50), version VARCHAR(50), manufacturer VARCHAR(100) ); CREATE TABLE IF NOT EXISTS implant_session ( id BIGINT PRIMARY KEY, civilian_id BIGINT, cyberware_id BIGINT, installed_at DATE, installed_by VARCHAR(250), CONSTRAINT fk_civilian FOREIGN KEY (civilian_id) REFERENCES civilian(id), CONSTRAINT fk_cyberware FOREIGN KEY (cyberware_id) REFERENCES cyberware(id) ); As a result, if you run Flyway with a new database, it will start with V4 migration, skipping previous versioned migrations. Undo Migrations (Teams Edition) Undo migrations are the commercial offering in Flyway Teams. They are aimed at the explicit reversal of a versioned migration with the same version as the undo migration. The naming convention is U__.sql, where the version mirrors the V file version it undoes. For example, if we wanted to delete the test data from the database, we would write a file called U2__undo_seed_data.sql with the following content: -- Undo for V2_seed_data DELETE FROM implant_session WHERE id = 1; DELETE FROM cyberware WHERE id = 1; DELETE FROM civilian WHERE id = 1; If you need to undo more versioned files, you write more undo migrations. Undo migrations work under the presumption that the whole migration succeeded and should be undone. But in some cases, the migration can fail at some points. For instance, you have five statements, but the second statement has failed. Undo migrations will not be helpful in such situations. Undo migrations are useful for fast local iteration, but production strategy should be based on forward-only fixes. In this case, you write new V files describing all required removals. This will help to avoid data loss and keep the audit clean. So, the alternative to the undo file above would be a V4__purge_bad_seed_data.sql file with the following content: -- Forward fix: remove test data inserted by V2 DELETE FROM implant_session WHERE id = 1; DELETE FROM cyberware WHERE id = 1; DELETE FROM civilian WHERE id = 1; Executing Migrations Conditionally In some cases, you may want to run some checks before applying the migration: for instance, verify that the column doesn’t exist. Flyway enables you to execute migrations conditionally, which means that the SQL inside a file decides whether the file does anything. There are three approaches to running migrations conditionally with Flyway: Conditional statements, Wrap the logic in a controlled block, Use placeholders. Let’s look at all of them. Conditional statements can be seamlessly woven into SQL and are most optimal for checking whether the DDL already exists: CREATE INDEX IF NOT EXISTS idx_email ON civilian(email); For more complex checks, you can use controlled code blocks, for instance, use PostgreSQL’s DO $$ … $$, which executes an anonymous code block. For example, let's add a new V migration V5__idx_birth_date_if_table_exists.sql for creating an index only when both table and column exist. Here, the SQL runs only if both conditions are met: DO $$ BEGIN -- Check table 'civilian' exists IF EXISTS ( SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 'civilian' AND c.relkind = 'r' -- ordinary table ) THEN -- Check index is absent IF NOT EXISTS ( SELECT 1 FROM pg_class c WHERE c.relname = 'idx_civilian_birth_date' ) THEN CREATE INDEX idx_civilian_birth_date ON civilian (birth_date); END IF; END IF; END $$; You can also use placeholders. We have already seen a placeholder with a timestamp in a repeatable migration file, but placeholders can also be used to create conditions. Let’s add a condition on seeding the test data only if explicitly enabled. Declare the placeholder in the application.properties file, for instance, seed_demo_data. Set it to true or false: spring.flyway.placeholders.seed_demo_data=true After that, we need to reference this placeholder in the migration file. Create a new file V6__conditional_demo_seed.sql. In this file, we will also use the Postgres DO block, but instead of writing the lengthy precondition ourselves, we simply use the placeholder with the IF statement: DO $$ BEGIN IF '${seed_demo_data}' = 'true' THEN INSERT INTO civilian (id, legal_name, national_id, birth_date, email, criminal_record, under_surveillance) VALUES (1, 'Jax Ortega', 'XP‑771203‑VT', CURRENT_DATE, 'ortega@xqa.li', FALSE, FALSE); INSERT INTO cyberware (id, name, type, version, manufacturer) VALUES (1, 'OptiSight X3', 'ocular', '1.1', 'SynthForge'); INSERT INTO implant_session (id, civilian_id, cyberware_id, installed_at, installed_by) VALUES (1, 1, 1, CURRENT_DATE, 'MP-129854'); END IF; END $$; Flyway will substitute seed_demo_data with true or false at runtime. As a result, in the non-production environment, you can get demo rows. On the other hand, you can set this placeholder to false for production, and in this case, Flyway will immediately exit leaving the database untouched. Using Flyway from CLI You can run Flyway from CLI as well. Let’s see how we can achieve that. First, add the flyway-maven-plugin to pom.xml and specify the user, password, and URL. Optionally, specify the schema: org.flywaydb flyway-maven-plugin 11.10.4 neurowatch_user mypassword jdbc:postgresql://localhost:5432/neurowatch-flyway neurowatch-flyway After that, you can run the Flyway commands. Flyway supports five basic commands to manage database migrations: info prints information about the current database version, pending migrations, run migrations and so on. migrate migrates a database schema to the current version. baseline takes the snapshot of the current database schema. It is useful when you need to start using Flyway with the existing database. validate validates current database schema against available migrations. repair repairs metadata table. clean drops all objects in the schema. Never use it in production! For instance, if you run mvn flyway:info You will see the summary of the statistics for the database migrations in the console. This command is very useful to know what's going on with your database migrations. Running Database Migrations in CI with Flyway and GitHub Actions Finally, let’s see how we can use Flyway to run database migrations in CI. For that purpose, we will use GitHub Actions. Before we write the workflow file, we need to adjust the settings in the Flyway plugin. Namely, we need to add our placeholders: org.flywaydb flyway-maven-plugin 11.10.4 neurowatch_user mypassword jdbc:postgresql://localhost:5432/neurowatch-flyway true ${env.BUILD_TS} Now, we can move on to the workflow file and discuss it step-by-step. The workflow YAML will be automatically triggered whenever changes are pushed to the main branch of the application: name: Flyway Migrations on: workflow_dispatch: push: branches: - main Next, we make sure that only one run is performed per branch/ref at a time, which prevents two migrations racing the same DB. A new run will queue instead of canceling the old one: concurrency: group: flyway-migrations-${{ github.ref }} cancel-in-progress: false After that, we spin up the PostgreSQL service. The database user and password are specified explicitly for the sake of the demo, but in a real setup, you should store credentials as GitHub Secrets and reference them in the password fields. The service also exposes the required ports and includes health-check options, so the job continues only after the database reports itself as ready. jobs: migrate: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_DB: neurowatch-flyway POSTGRES_USER: neurowatch_user POSTGRES_PASSWORD: mypassword ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 Next, we specify job-wide environment variables applicable to all following steps: JDBC URL, credentials matching the service container, and the timestamp placeholder. This way, for repeatable migrations that reference ${build_timestamp}, Flyway will substitute this value. The value stays constant for the whole workflow run: env: FLYWAY_URL: jdbc:postgresql://localhost:5432/neurowatch-flyway FLYWAY_USER: neurowatch_user FLYWAY_PASSWORD: mypassword FLYWAY_PLACEHOLDERS_BUILD_TIMESTAMP: "${{ github.run_id }}-${{ github.run_attempt }}" The first job steps are checking out the repository and running a resource processing phase with Maven to filter config files and prepare assets. It doesn’t do anything if there’s nothing to process. After that, we install Liberica JDK 24, set JAVA_HOME, and enable Maven dependency caching for speed: steps: - name: Checkout repository uses: actions/checkout@v4 - name: Process Resources shell: bash run: | ./mvnw process-resources - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: liberica java-version: '25' cache: maven The next step runs the Flyway Maven plugin to apply pending migrations. It connects to the DB using the credentials specified above, creates flyway_schema_history if missing, runs V files in order, then any R files whose checksum changed. The -B flag means the batch mode (no interactive prompts). - name: Flyway migrate and validate run: mvn -B flyway:migrate The following step verifies that the database matches the migration files (checksums/order). - name: Flyway validate run: mvn -B flyway:validate Once migrations are applied, the workflow runs the tests. If everything passes, the final stage deploys the application. In this demo, the last step is represented by a simple placeholder command, but in a real pipeline, you would replace it with your actual deployment process. - name: Run tests run: ./mvnw test - name: Deploy app if: success() run: echo "Deploy your app here..." That’s it! The whole file can be found on GitHub. Conclusion In this article, we examined using Flyway with Spring Boot and Maven for reliable database migrations locally and in the CI/CD pipeline. Don’t forget to subscribe to our newsletter for a monthly digest of our articles and videos on Java development and news in the Java world! - [Liberica JDK 8u472, 11.0.29, 17.0.17, 21.0.9, and 25.0.1 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u472-11-0-29-17-0-17-21-0-9-and-25-0-1-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u481, 7u481, 8u471, 11.0.28.0.1, 17.0.16.0.1, 21.0.8.0.1, 25.0.0.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u472, 11.0.29, 17.0.17, 21.0.9, and 25.0.1 with non-critical fixes and general improvements. The release contains 687 fixes and backports overall. BellSoft participated in eliminating 13 issues in all releases. Download Liberica JDK How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 4 security issues (CVEs) fixed. 23 total security fixes in CPU release: in Liberica 6u481: 3 security fixes; in Liberica 7u481: 3 security fixes; in Liberica 8u471: 3 security fixes; in Liberica 11.0.28.0.1: 3 security fixes; in Liberica 17.0.16.0.1: 3 security fixes; in Liberica 21.0.8.0.1: 4 security fixes; in Liberica 25.0.0.0.1: 4 security fixes. In addition, PSU releases include a total of 664 fixes and backports: in Liberica 8u472: 3 security fixes (+ 1 in FX) + 31 additional fixes (+ 2 in FX); in Liberica 11.0.29: 3 security fixes (+ 1 in FX) + 35 additional fixes (+ 1 in FX); in Liberica 17.0.17: 3 security fixes (+ 1 in FX) + 257 additional fixes (+ 3 in FX); in Liberica 21.0.9: 4 security fixes (+ 1 in FX) + 281 additional fixes (+ 3 in FX). in Liberica 25.0.1: 4 security fixes (+ 1 in FX) + 23 additional fixes (+ 6 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2025-53057 5.9 security-libs java.security network high none none unchanged none high none CVE-2025-53066 4.8 xml jaxp network high none none unchanged low none low CVE-2025-61748 3.7 core-libc network high none none unchanged none low none CVE-2025-31257 7.5 javafx web network high none required unchanged high high high Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 25 CVE-2025-53057 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-53066 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-61748 𑇐 𑇐 CVE-2025-31257 𑇐 𑇐 𑇐 𑇐 𑇐 Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Liberica Native Image Kit 23.0.10, 23.1.9, and 25.0.1 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-10-23-1-9-and-25-0-1-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.10 for JDK 17, 23.1.9 for JDK 21, and 25.0.1 for JDK 24 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable improvements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2025-53057 5.9 security-libs java.security network high none none unchanged none high none CVE-2025-53066 4.8 xml jaxp network high none none unchanged low none low CVE-2025-61748 3.7 core-libc network high none none unchanged none low none CVE-2025-31257 7.5 javafx web network high none required unchanged high high high Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [Docker Image Security: Best Practices for Production](https://bell-sw.com/blog/docker-image-security-best-practices-for-production/): Docker images are not just artifacts, they’re part of your software supply chain. They can be copied, scanned, and exploited, putting the whole system at risk. Once a compromised image reaches production, the damage goes far beyond a single CVE: stolen credentials, privilege escalation on the host, or data exfiltration through mounted volumes. These attacks often start from something as trivial as an outdated base image or a misconfigured Dockerfile. In this article, we’ll go through 11 proven Docker image security practices you can immediately apply in your Dockerfiles, CI/CD pipeline, and Kubernetes nodes: from minimizing attack surface and dropping privileges to generating SBOMs, verifying image provenance, and hardening the host. Table of Contents Keep your Container Images Lean No Package Manager if Possible Run as Non-root and with Minimum Priviledges Don’t Tweak Containers in Prod Strive Towards Deterministic Container Images Implement an SBOM and Provenance No Do-It-Yourself Approach for Base Images Use LTS Versions and Update Base Image Regularly No Secrets in Containers Use Security Scanners Implement Host Hardening Conclusion FAQ: Docker Image Security Keep your Container Images Lean The smaller the attack surface in your production image, the better. However, the attack surface is not a sum of all components in your image. Rather, it is a sum of attack vectors, potentially exploitable paths. Therefore, try to keep to a minimum components sitting in the direct execution path. Depending on your app, consider using images based on a minimal Linux distribution, distroless images, or scratch. Use multi-stage builds to transfer the app into a final base image without unnecessary components. Here’s the example of a multi-stage Dockerfile for Java. It uses Liberica Runtime Container with JDK to build the application; then, we copy the JAR file into a final image based on Liberica Runtime Container with JRE and run it: FROM bellsoft/liberica-runtime-container:jdk-25-musl AS builder WORKDIR /app ADD my-java-app /app/my-java-app RUN cd my-java-app && ./mvnw package FROM bellsoft/liberica-runtime-container:jre-25-musl WORKDIR /app COPY --from=builder /app/my-java-app/target/*.jar app.jar ENTRYPOINT ["java", "-jar", "/app/app.jar"] And here’s an example for Go that uses distroless as a final base image: FROM golang:1.24.4 AS builder WORKDIR /app COPY go.mod main.go ./ RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -o /hello FROM gcr.io/distroless/static-debian12 WORKDIR / COPY --from=builder /hello /hello ENTRYPOINT ["/hello"] No Package Manager if Possible A package manager in production kills immutability and widens the attack surface. If someone installs packages in production, the container no longer matches the image you built, scanned, and signed. As a result, an SBOM, signature, and provenance lose their power. Plus, you can’t reproduce or roll back cleanly. Attackers can use the package manager to install specific tools for malicious activities, especially if you run your container as root or with additional capabilities. In addition, it is easier to install packages with CVEs. Therefore, opt for a slim final base image without a package manager if possible. Run as Non-root and with Minimum Priviledges Running the container as root or giving it a full set of privileges will give the intruders a host-level access to container resources and the ability to exercise kernel attacks. Therefore, Kubernetes and Docker security standards strongly advise running containers as non-root or rootless to limit the blast surface if the container is compromised. So, set an unprivileged user and group in the Dockerfile to run the container. If you don’t set the user, Docker runs as root by default. Therefore, you should explicitly set the user if possible. You can do that by creating a custom user with specific UID and GID: USER 1234:1234 Or RUN groupadd -r myuser && useradd -r -g myuser myuser USER myuser As for the privileges. Even if you configure your containers to run as non-root or rootless, it is recommended not to run the containers with the --privileged flag if you don’t need it for a purpose, because it gives ALL Linux kernel capabilities to the container. Limit the granted privileges only to those needed by the container. You can run --cap-drop all to drop all capabilities first just to be sure, and then add capabilities with --cap-add. docker run --cap-drop all --cap-add Don’t Tweak Containers in Prod The container must correspond to the container image. It means no patches, config tweaks, or quick code fixes in production. If something changes, you rebuild and redeploy the new artifact. But what does it have to do with security? Runtime integrity: the SBOMs and signatures actually describe what’s running in prod. Easier rollback: if something suspicious happens, you simply kill the process and redeploy. Fewer opportunities for tampering: the absence of the package manager and restricted privileges gives attackers less mutation paths. There are several approaches that can help you build more secure images, including: Multi-stage builds, where the toolchain is used only in the builder. Slim final base images without the package manager. The runtime settings that prevent privilege escalation. In addition, store the stateful data like databases separately and don’t persist data in the container. You can use the --mount or the --tmpfs flag to create files outside the container: --tmpfs OR --mount type=tmpfs,dst= To prevent privilege escalation at runtime, you can use the --security-opt no-new-privileges option: --security-opt no-new-privileges You can also run the container with the read-only filesystem if your setup allows you to do so: --read-only Strive Towards Deterministic Container Images Determinism should go hand-in-hand with the aforementioned approach. Deterministic images mean that given the same input, the build produces the exact same bytes. This way, you can detect tampering or dependency change between build runs. It can also help to eliminate the Time-of-Check to Time-of-Use (TOCTOU) vulnerability when the attacker has time to manipulate the resource in the time interval between when the system checks the state of resource and uses it. How do we get deterministic images? Pin everything: toolchain, build system, and OS version. Importantly, pin the base image by digest, not by tag, because tags can drift, but the digest always points to the image you used in the first place. For example, instead of pinning Liberica Runtime Container by version: bellsoft/liberica-runtime-container:jre-25_37-slim-musl You should pin it by digest and specify the version in a comment: #base image version: bellsoft/liberica-runtime-container:jre-25_37-slim-musl bellsoft/liberica-runtime-container:sha256:5646cf896dafe95def30420defa8077fc8ee71ef5578e2c018c2572aae0541e2 Implement an SBOM and Provenance One more powerful approach to increasing the security of container images is integrating a Software Build of Materials or an SBOM. It is a document compiled in machine- and human-readable format that provides the data on the components, libraries, and modules that were used for building a given piece of software. Basically, an SBOM says what’s inside your container image. An SBOM includes direct and indirect dependencies on open source and proprietary software. The data on each component includes but is not limited to its name, version, licence, unique identifiers if any. An SBOM prevents hidden vulnerabilities from nesting in your project and accelerates CVE remediation because you can easily map a new CVE to the affected image. An SBOM is the first line of defence against software supply chain attacks. No wonder that many legislations demand an SBOM adoption. There are several open source tools for generating SBOMs: Syft, CycloneDX Generator, for instance. Optionally, you can generate an SBOM at the pre-build stage to check the dependency state. But generating an SBOM at the build stage is a must. In this case, you will get the list of exactly what you ship and run. Here’s an example of generating an SBOM for a container image: syft $IMG --output cyclonedx-json=oci-sbom-syft.json If you ship a Java application, this article describes generating an SBOM for Java apps using various approaches and tools in more detail. However, producing an SBOM is not enough. You need to provide the written proof of software provenance and that the SBOM indeed reflects the contents of the image. Provenance is a set of processes aimed at proving where the container image came from by providing metadata about where, when, and how the artifact was built. This data includes builder identity, source repo commit, build steps, and so on. In other words, provenance together with an immutable digest lets you verify: “This image was built by our CI from commit X with builder Y.” This helps to avoid tag-swap and time-of-check to time-of-use (TOCTOU) vulnerabilities. Provenance processes and tools are defined by the Supply-chain Levels for Software Artifacts (SLSA) framework. So, how do we combine SBOMs and provenance? In comes the attestation! It is a cryptographically signed statement about some property of an artifact, tied to its immutable digest. In other words, it is metadata that consumers can verify independently of the software producer. The workflow can look as follows: Generate an SBOM and implement provenance for every image. Store an SBOM and provenance data as attestations tied to the image digest. Sign images. At deploy, verify signature and provenance. You can use Cosign for attestation. For instance, let’s attest the SBOM we generated earlier: cosign attest --predicate oci-sbom-syft.json --type cyclonedx $IMG The software consumer then verifies the signature of the artifact and the required attestations (provenance, SBOM) in their CI/CD. Depending on the verification results, you can Accept the image and deploy it, Quarantine it for manual review if, for instance, you detect unverifiable signer, a stale SBOM, or medium vulnerabilities with compensating controls. Reject the image in case it is unsigned, or you detect wrong identity/issuer, missing/forged attestation, wrong source/ref, or critical vulnerabilities for which the patches are available but not implemented in the image. No Do-It-Yourself Approach for Base Images Quite a few teams build their images on some random base. Unfortunately, even if such base images don’t cause any performance regressions in your project, they introduce severe security risks: CVE blind spots, late or no patches for various community-driven components; No provenance and weak trust; No vendor-backed SLA; No compliance with the legal requirements to use trusted base images. Therefore, opt for a well-maintained, regularly-updated base image from a trusted vendor. At BellSoft, we provide container images for Java applications based on the products we develop and support: minimalistic Alpaquita Linux and Liberica JDK. The images come with an SBOM and can be easily verified by the checksum. Pin the base image by digest and verify signatures and provenance before building the application, for instance, using the Cosign tool mentioned in the section above: cosign verify \ --certificate-identity "$EXPECTED_IDENTITY" \ --certificate-oidc-issuer "$EXPECTED_ISSUER" \ @sha256: Use LTS Versions and Update Base Image Regularly Consider using base images with LTS software versions. Long-Term Support (LTS) software releases are supported for several years and have clearly defined security update policies, with security patches and critical fixes backported from the newest versions. Furthermore, less vulnerabilities appear in LTS versions as compared to non-LTS ones. Below you can find the support cycles for several operating systems and programming languages: Node.js LTS releases see the light every year in April or May and receive support with security patches and critical patches for 2.5 years. Java LTS versions are released every two years, the support period depends on the JDK vendor. Ubuntu LTS versions are released every two years and receive patches for 5 years. Alpine doesn’t have a fixed release cycle, but usually, the edge release is snapshotted every 6 months and receives security patches for two years. Alpaquita LTS versions come out every two years and are supported for four years. Regardless of whether you use LTS or non-LTS software versions, if you don’t update the base image, you accumulate known vulnerabilities and extend the attack surface. For example, the OS layer gets new CVEs constantly. The good news is that reliable vendors also update their images constantly. Not so good news: when a vendor releases an update for your base image, it won’t get updated in your builds auto-magically. Therefore, you need to set up automatic updates monitoring using such tools as, for example, Dependabot or Renovate. After that, you must rebuild, rescan, and resign the image so your SBOM and signatures reflect the patched contents. Otherwise, your trusted artifact cannot be considered trusted anymore. No Secrets in Containers A container image is a distribution artefact. Anything you store there can be cached, copied, and extracted. Therefore, secrets baked into the image are a serious security risk. Use dedicated secret management tools like HashiCorp Vault, Google Secrets Manager or Kubernetes Secrets to keep secrets there, and then fetch them at runtime. Also, prefer files over environmental variables because the latter are more easily leaked to child processes, crash logs, and so on. On the other hand, files allow for the principle of least privilege. They can be mounted as read-only to a non-root process with locked egress. Use Security Scanners Scanners are automated tools that can analyze the application code, a container image, configs, SBOMs, etc. to detect security flaws. If a Software Bill of Materials answers the question “What’s in the image?” the scanners answer “How risky is it?” New CVEs appear daily, and scanners map them to your artifacts automatically using an SBOM inventory. But some risks aren’t related to CVEs. They include writable file systems, dangerous capabilities, bad permissions, or exposed secrets in env/files. Scanners or lint tools can catch such risks as well. Scanners vary in purpose, so you can use them at different stages of the software lifecycle. The table below summarizes the deployment stages where scanners can be useful and provides several scanning tools fit for the purpose: Stage Process Scanners Pre-commit / pre-PR Scan for secrets to stop credentials from landing in Git Gitleaks TruffleHog CI build Analyze the dependencies by scanning the image and/or an SBOM OSV-Scanner Trivy Grype Clair Post-publish Perform continuous rescans of pushed images and/or SBOMs as advisories update Runtime Monitor for malicious activities Scan nodes/clusters to to flag running workloads when new CVEs land Trivy Operator Falco Scanners supply evidence, policies pull the trigger. You should define policies for acting upon the scanning results, for instance, block or allow the build/deploy, produce an additional artifact, perform manual review, issue notifications of suspicious activities, etc. Implement Host Hardening Containers share the host kernel. If the kernel is vulnerable, everything on it is vulnerable. So, host hardening is also an indispensable part of container security enhancement. You can use a dedicated OS for Kubernetes nodes such as Bottlerocked or a minimal OS such as Alpaquita LTS. The key is that the system should be immutable and without any spare components. Update the kernel on a regular basis. Also, enable Linux Security modules such as AppArmor or SELinux to set the required security policies. Therefore, when some risky kernel operation is detected, the LSM decides whether to allow or deny it as per defined policies. Combine it with seccomp (syscall filter) and capabilities to enforce least privilege. Conclusion In this article we looked into the key principles of securing Docker container images. Here’s what we covered: Keep container images lean and deterministic, Use a trusted base image, Prove the contents with an SBOM and provenance, Implement security scanners to monitor for CVEs and other security flaws, Harden the Linux Host. Each of the practices described here can be expanded into a separate article. Therefore, we will continue to explore the topic in the following articles. Subscribe to our newsletter so as not to miss them! FAQ: Docker Image Security How do I generate an SBOM for a Docker image? You can generate an SBOM for a Docker image using open source tools such as Syft or CycloneDX Generator. You can use a Maven or Gradle plugin to generate an SBOM at build time or use the tool to generate an SBOM for a ready container image. Here’s an example using Syft: syft $IMG --output cyclonedx-json=oci-sbom-syft.json Should containers run as root? Docker and Kubernetes security standards do not recommend running containers as root because it can give malicious actors host-level access to container resources and increase the severity of attacks. How often should I rebuild my base image? The base image should be rebuilt every time the updated version is released. You can automatically track updates with open source tools such as Dependabot. What is image provenance and why should I care? Provenance is a set of processes that enable the software consumers to verify that the container image is trustworthy and wasn’t tampered with by providing metadata about where, when, and how the container image was built. Provenance increases the transparency of software and helps to ensure its integrity, which is critical for protecting the software supply chain. - [BellSoft's Hardened Images Set New Standard for Container Security](https://bell-sw.com/news/bellsoft-s-hardened-images-set-new-standard-for-container-security/): Pioneering 3-in-1 approach combining Java runtime optimization, OS hardening, and proactive CVE remediation delivers 95% fewer CVEs, 30% resource savings, and simplified compliance for enterprise development teams San Jose, California (November 10, 2025) BellSoft, the OpenJDK vendor delivering the most complete Java experience, announces Hardened Images, a tool for enhancing the security and compliance of containerized applications in Kubernetes. BellSoft will be demonstrating Hardened Images at KubeCon 2025 in Atlanta, beginning today. The Container Security Challenge Demands Action Two-thirds of organizations experienced a container security incident in the past year. The challenge runs deeper than misconfigurations, it's rooted in the software supply chain itself. A typical container image carries over 600 known vulnerabilities, nearly half of them years old. For Java workloads, the risk is particularly acute: 44% of Java services contain known-exploited vulnerabilities, compared to 5% for Go and just 2% for other languages. Container hardening addresses these challenges by systematically securing images, configurations and runtimes from the start. By embedding security controls into every stage of the container lifecycle, organizations can protect workloads against misconfigurations, unauthorized access and supply chain risks while meeting increasingly stringent regulatory requirements. BellSoft Hardened Images Reduce Risk and Simplify Compliance BellSoft Hardened Images are minimized container images that remove package managers and non-essential components, significantly reducing vulnerabilities and limiting potential attack vectors. These images feature a locked configuration that cannot be modified, minimizing the risk of attackers introducing malware or tampering with the runtime environment. BellSoft’s images are based on Alpaquita Linux, a lightweight open-source distribution designed with BusyBox and APK. Alpaquita is offered in both musl and glibc variants. Combined with continuous CVE patching, industry-standard OSV schema vulnerability reporting and enterprise support, BellSoft Hardened Images provide a secure, audit-ready foundation that protects businesses, integrates seamlessly with existing security tools and frees up developer time. The Security Advantages of BellSoft Hardened Images: Reduced Attack Surface: Hardened Images remove unnecessary packages and components that often bloat containers, minimizing potential vulnerabilities and limiting exploitable entry points for attackers. Near-Zero CVEs: BellSoft’s solution delivers continuous monitoring, proactive patch development and timely security updates, helping organizations maintain containers with virtually no known vulnerabilities. Runtime Protection: Hardened Images feature an immutable component set that prevents unauthorized modifications, malware installation and tampering, ensuring that the runtime environment remains secure and stable. Enhanced Compliance: The solution includes a detailed Software Bill of Materials (SBOM), simplifying audits and supporting regulatory and security compliance requirements with minimal effort. BellSoft’s 3-in-1 Approach Delivers 95% Fewer CVEs and Superior Java Performance BellSoft Hardened Images stand out among container security solutions through a comprehensive “3-in-1” approach that combines expert Java runtime optimization, custom Alpaquita Linux maintenance and proactive CVE remediation for complete security and performance coverage. Unlike competitors who rely on upstream patch releases, BellSoft’s in-house security expertise enables the rapid creation of custom patches when needed, keeping environments secure without delay. For Java workloads, organizations migrating from other distributions can see up to a 30% reduction in network, disk space and RAM usage with Liberica JDK Lite, delivering tangible performance gains. Additionally, Alpaquita Linux’s dual support for both musl and glibc libraries offers unmatched flexibility, allowing seamless migration without code rewrites regardless of existing system architecture. Competitive analysis shows over 95% fewer CVEs with BellSoft Hardened Images as compared to standard Java images. “BellSoft Hardened Images is a response to the growing demand in the market for secure, high-performance container solutions and is consistent with our commitment to providing the most complete and reliable Java experience,” said Aleksei Voitylov, CTO at BellSoft. “By combining expert Java runtime optimization, proactive CVE remediation and a lightweight, flexible foundation, our hardened images give organizations a secure, audit-ready platform that reduces vulnerabilities, improves performance and simplifies migration. This solution not only protects critical workloads from emerging threats but also frees developers to focus on innovation rather than managing security risks, delivering measurable value across IT and business operations.” Availability and Pricing BellSoft Hardened Images are available now in three tiers: Community -- offering free Hardened Liberica Runtime Containers (JDK LTS 21 and 25+) on Docker Hub Standard -- providing hardened images across all JDK versions, GraalVM, Go, Python, C and Alpaquita base with 7-day critical CVE remediation SLA and custom image development Premium -- adding technical support and performance optimization consulting Learn more at https://bell-sw.com/bellsoft-hardened-images/ - [Liberica JDK 8u472, 11.0.29, 17.0.17, 21.0.9, and 25.0.1 builds are updated with important patches](https://bell-sw.com/blog/liberica-jdk-8u472-11-0-29-17-0-17-21-0-9-and-25-0-1-builds-are-updated-with-important-patches/): We have released Liberica JDK builds 8u472, 11.0.29, 17.0.17, 21.0.9, and 25.0.1 with patches for four critical vulnerabilities found in the OpenJFX. The severity of these CVEs is high or medium, so we recommend updating the JDK as soon as possible if you use OpenJFX in your projects. Download Liberica JDK Below you will find more detailed information about the vulnerabilities. Another important fix solves the issues of absent classes*.jsa archive on Linux AArch64 when using CDS. List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2025-7424 7.8 javafx libxslt local high none none changed none high high CVE-2025-7425 7.8 javafx libxslt local high none none changed none high high CVE-2025-6021 7.5 javafx libxml2 network low none none unchanged none none high CVE-2025-10911 5.5 javafx libxslt local low none required unchanged none none high Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 25 CVE-2025-7424 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-7425 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-6021 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-10911 𑇐 𑇐 𑇐 𑇐 𑇐 - [BellSoft Releases Alpaquita Linux 25 LTS](https://bell-sw.com/blog/bellsoft-releases-alpaquita-linux-25-lts/): We are happy to announce the general availability of Alpaquita Linux 25, which is a Long-Term Support (LTS) version that will receive the functional fixes and security patches until 2029. Alpaquita Linux 25 deliverables include ISO, minirootfs, microVM images, Docker images, Hardened Docker images, package repositories, and docker images. The following platforms are supported: Intel (x86-64-v2) AMD 64-bit (x86-64-v2) AArch64 (ARMv8-A) This release includes many updates, as well as a number of bug and security fixes, the most essential of which are summarized below. A more detailed description of these and other changes can be found in the release notes. Install New Alpaquita Linux Version Table of Contents Summary of Key Enhancements Updated Kernel to 6.12 OSV-Scanner Package for Security Scanning and SBOMs The microVM Images The x86-64 packages built with -fno-plt Updated glibc Updated musl Conclusion Summary of Key Enhancements Updated Kernel to 6.12 Alpaquita Linux 25 is based on a new LTS kernel version 6.12, which is optimized for smaller memory footprint, stronger security, and better performance, namely: Lower latency and better responsiveness, Compressed modules to save disc space, Removed several old modules with improper support or known CVEs. OSV-Scanner Package for Security Scanning and SBOMs For this release, BellSoft has adapted the OSV-scanner version that can scan OS images for security issues and produce a Software Bill of Materials (SBOM) as a result. This version fully supports the BellSoft ecosystem, i.e., Alpaquita Linux and BellSoft Hardened Containers. Owing to this update, security scanning and SBOM generation can be seamlessly integrated into the enterprise security practices when using Alpaquita Linux for enhanced transparency, compliance, and better security management. For more information on using the tool with Alpaquita, see Getting Started with OSV-Scanner for Alpaquita Linux. The microVM Images Alpaquita Linux works seamlessly with Firecracker VM, an open-source virtualization technology for creating secure and lightweight virtual machines (microVMs). The microVMs combine the security and isolation of a virtual machine with the resource efficiency of a container. Therefore, they are a good match for serverless functions such as AWS Lambdas and other short-lived workloads. This release of Alpaquita Linux includes a pre-built microVM vmlinux and rootfs, ready to use with FirecrackerVM and QEMU. See Alpaquita Linux: Using Firecracker and QEMU microVMs for more information. The x86-64 packages built with -fno-plt The x86-64 packages are now built with -fno-plt by default. This change reduces the overhead of function calls to shared libraries by avoiding the Procedure Linkage Table (PLT), resulting in slightly more efficient code, faster startup and lower call-latency. Updated glibc The glibc library in Alpaquita Linux 25 was upgraded to version 2.39, which includes several new functions and enhancements, including New tunables for x86-64 PLT rewrite, Richer memory map details, Vector math library libmvec support added to AArch64, New spawn-related functions, including cgroupv2-safe and pidfd-based variants, and Handy string functions strlcpy and strlcat. In addition, the utmps library, which provides implementation of utmp/wtmp functions, was removed from the glibc variant of Alpaquita, because glibc natively supports the utmp/wtmp functionality. Updated musl Alpaquita Linux comes with two musl variants: musl default and musl-perf, which is 100% compatible with the default version but has better performance. Both packages were upgraded to the musl release 1.2.5 with new functions and changes, including: New statx function for more detailed file info, New preadv2/pwritev2 functions with extra flags to fine-tune behavior per call, Updated printf family to better match newer standards. Apart from that, musl-perf package has switched to the high-performance allocator implementation mimalloc v2 release, replacing the default allocator in musl, known as mallocng. As a result, the users using musl-perf should see the improved performance for all types of workloads. Other updates to musl-perf include: glibc-2.39 memory function implementations, ldd can now detect static-pie binaries to avoid printing misleading information about required shared objects. Conclusion This release of Alpaquita Linux makes it an even stronger option for lightweight, secure, and cloud-friendly environments. Get the latest musl or glibc build now and start your next deployment with a smaller, faster base. Get Alpaquita Linux 25 - [BellSoft 2025 Recap ](https://bell-sw.com/blog/growing-stronger-how-bellsoft-and-our-community-shaped-2025/): 2025 was a year of growth, collaboration, and communication. This was a year marked not just by technical milestones, but by the deepening relationships with our community, the exceptional work of everyone at BellSoft, and the collaborative spirit of our partners. To our team, our partners, and everyone in the Java community who has been part of this journey: thank you. Your expertise, feedback, and collaboration have been the foundation of everything we've achieved this year. Three Decades with Java This year, Java celebrated its 30th anniversary—a milestone that's deeply personal for us at BellSoft. Some members of our team—Aleksey, Valery, and Peter—worked at Sun Microsystems building the JDK itself, and still have Java 1.0-era commemorative plaques on their shelves. To mark this occasion, we released a mini-series where we unwind Java 23 code all the way back to Java 1.0. It resonated with developers who remember those early days, and reminded all of us why we fell in love with this language. If you haven't watched it yet, check out the series—it's a fun journey through Java's evolution and how the developer experience has transformed. Building the Most Complete Java Experience Our mission has always been clear: deliver the most complete Java experience. When developing our products, we focus on three core pillars—performance, security, and reliability. This year, we made significant strides toward that goal. Java 25 and the Path Forward With the release of Java 25 LTS, the Java ecosystem took another major step forward. An important milestone this year was GraalVM being detached from the Java release train in September 2025—a significant shift in the ecosystem. Performance That Matters Performance isn't just a bullet point for us—it's a core commitment. This year, we upgraded Liberica JDK Performance Edition to JVM 21 for both Java 8 and Java 11 builds. The results speak for themselves: 5-10% better performance in most workloads, with some cases showing gains up to 40%. We understand that many organizations remain on Java 8 or 11 not out of neglect, but because these versions are stable, proven, and deeply integrated into critical systems. Our Performance Edition provides a practical path forward—unlocking modern JVM performance without requiring code changes or risky migrations. Security at the Foundation Security is non-negotiable. This year's launch of BellSoft Hardened Images represents the culmination of months of work focused on filling a critical gap in the market. Our 3-in-1 approach—build, maintain, and support at both OS and runtime levels—delivers near-zero CVE container images while providing the resource optimization benefits of switching to optimized JDK distributions. Our recent survey revealed that 49% of organizations cite time and resource constraints as a primary challenge in maintaining container security. When asked "What would most help your organization improve container security?" the number one response was pre-hardened images. BellSoft Hardened Images addresses both needs while delivering tangible resource efficiency benefits—reduced RAM usage and disk space—when migrating from other JDKs to Liberica. We're proud that this solution has already generated strong interest from enterprises looking to modernize their container security posture. A Stronger Foundation with Alpaquita Linux 25 LTS We released Alpaquita Linux 25 LTS with 6 years of support ahead. Built on kernel 6.12 and updated with glibc 2.39 and musl 1.2.5, this release delivers enhanced security, better performance, and improved memory efficiency. For teams building cloud-native applications, Alpaquita continues to prove itself as a lightweight, secure, and reliable foundation, and a enterprise alternative to Alpine linux. Trust from the Community The strongest validation of our work comes from the community. This year, Liberica Runtime Containers were pulled 4x more times compared to 2024. Alpaquita downloads doubled. Liberica JDK downloads increased by 55%. When developers vote with their infrastructure choices at this scale, it tells us we're on the right path. We're honored that Liberica JDK was recognized in both the G2 Fall and Winter reports as a high performer—with a 100% satisfaction rate. That's not a metric we take lightly. It reflects the effort our engineering and support teams pour into every release, every patch, and every customer interaction. What speaks volumes about our product quality is client retention: once organizations sign with us for Liberica JDK, they stay. 100% retention. Zero churn. This year, we added several important names to our client portfolio, including major financial institutions that trust Liberica JDK with their most critical workloads. We see companies from around the world choosing our products. Each deployment is a vote of confidence, and we're deeply grateful for that trust. For the Community, By the Community Developer relations isn't just a department at BellSoft—it's part of our DNA. This year, our DevRel team has been everywhere, and their work has been exceptional. We launched Cyber Jar, our new YouTube channel, which has already got positive feedback from the community. The livestreams feature guests like Josh Long, Rod Johnson, Simon Martinelli, Frank Delporte and Siva Prasad Reddy, creating spaces for real conversations about Java, technology, and the future of development. We also relaunched JRush, our free live events where we share trends, knowledge, and practical coding insights. After kicking off in June, we repeated the success in November, and the response has been tremendous. Our Developer Advocates spoke at all major Java conferences across the US and Europe this year. Pasha Finkelshteyn, our Developer Advocate, spent 117 days on the road and traveled 128,000 km this year. That’s enough to circle the world 3.2 times! Their presence at these events ensures we're not just talking to the community—we're listening, learning, and staying connected with what developers actually need. Looking Ahead As I look at what we've accomplished in 2025, I'm struck by how much of it was a team effort. Our engineers who build and maintain the JDK. Our security experts who ensure every release meets the highest standards. Our Developer Relations team who keep us connected to the community. Our support team who helps customers succeed every day. And our partners who trust us with their most critical applications. None of this happens without people. The best technology in the world is meaningless if it doesn't solve real problems for real organizations. That's what drives us. As we move into 2026, we remain committed to what we do best: delivering the most complete Java experience. We'll continue to push the boundaries of performance and security. We'll keep investing in our community. And we'll maintain the stability and reliability that enterprise teams depend on. Thank you for being part of this journey. Here's to the year ahead. - [Liberica JDK 8u482, 11.0.30, 17.0.18, 21.0.10, and 25.0.2 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u482-11-0-30-17-0-18-21-0-10-and-25-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u491, 7u491, 8u481, 11.0.29.0.1, 17.0.17.0.1, 21.0.9.0.1, 25.0.1.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u482, 11.0.30, 17.0.18, 21.0.10, and 25.0.2 with non-critical fixes and general improvements. The release contains 1217 fixes and backports overall. BellSoft participated in eliminating 21 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 11 security issues (CVEs) fixed. 71 total security fixes in CPU release: in Liberica 6u491: 9 security fixes; in Liberica 7u491: 11 security fixes; in Liberica 8u481: 10 security fixes; in Liberica 11.0.29.0.1: 10 security fixes; in Liberica 17.0.17.0.1: 10 security fixes; in Liberica 21.0.9.0.1: 11 security fixes; in Liberica 25.0.1.0.1: 10 security fixes. In addition, PSU releases include a total of 1146 fixes and backports: in Liberica 8u482: 10 security fixes (+ 10 in FX) + 40 additional fixes; in Liberica 11.0.30: 10 security fixes (+ 10 in FX) + 39 additional fixes; in Liberica 17.0.18: 11 security fixes (+ 10 in FX) + 290 additional fixes (+ 4 in FX); in Liberica 21.0.10: 11 security fixes (+ 10 in FX) + 327 additional fixes (+ 4 in FX). in Liberica 25.0.2: 10 security fixes (+ 10 in FX) + 318 additional fixes (+ 22 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2026-21945 7.5 security-libs java.security network low none none unchanged none none high CVE-2026-21932 7.4 client-libs java.awt network low none required changed none high none CVE-2026-21933 6.1 core-libs java.net network low none required changed low low none CVE-2026-21925 4.8 core-libs java.rmi network high none none unchanged low low none CVE-2025-43368 7.5 javafx web network high none required unchanged high high high CVE-2025-7425 7.5 javafx web network high none required unchanged high high high CVE-2025-6021 5.9 javafx web network high none none unchanged none none high CVE-2025-7424 5.5 javafx web local low none required unchanged none none high CVE-2025-6052 3.7 javafx media network high none none unchanged none none low CVE-2026-21947 3.1 javafx web network high none required unchanged none low none CVE-2025-47219 3.1 javafx media network high none required unchanged low none none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 25 CVE-2026-21945 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-21932 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-21933 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-21925 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-43368 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-7425 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-6021 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-7424 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-6052 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-21947 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2025-47219 𑇐 𑇐 𑇐 𑇐 𑇐 Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [A Guide to Java Application Security](https://bell-sw.com/blog/a-guide-to-java-application-security/): The growing dependence on digital services is a fertile ground for cybercrime that affects millions of people worldwide and costs billions in losses every year. Most Java security incidents are caused by predictable engineering debt: vulnerable dependencies, weak boundaries, and zero visibility when something goes wrong. This article breaks down the specific best practices for securing modern Java applications. By the end, you’ll get a checklist that covers code, runtime, and containers. Table of Contents Java Security Best Practices Avoid Using JDK Internal Components Minimize Serialization Risks Apply Input Sanitation Techniques Take Advantage of Java API for XML Processing (JAXP) Use Strong Encryption and Password Hashing Be Careful with Error and Exception Handling Update the Runtime Quarterly Harden your Container Images Implement A Software Bills of Materials (SBOM) Conclusion: How to Stay Up-to-date with Security Measures Java Security Best Practices Avoid Using JDK Internal Components Modules were introduced in Java 9 to enhance Java integrity and protect internal JDK components from ubiquitous access. The process peaked with JEP 403: Strongly Encapsulate JDK Internals in JDK 17. However, versions 9 to 16 allowed developers to use reflection to access internal elements. If you are not planning to migrate to version 17 or newer in the near future, we strongly recommend avoiding reflection and migrating to standard APIs. Internal APIs may transfer sensitive data or define privileged operations, so external access compromises the platform’s security. Minimize Serialization Risks Java serialization enables the developers to convert an object into a byte stream for further transmission or storage. In turn, deserialization is used for converting byte streams back into original objects. The security issues are related to the deserialization part because methods of a deserialized object may be indirectly invoked before any checks are performed on reconstructed objects. As a result, a stream of unknown data becomes a Java object with potentially malicious data, leading to the risk of remote code execution. A series of exploited vulnerabilities related to deserialization proves that the serialization mechanism is inherently unsafe. MITRE’s CWE Top 25 (2025) still lists CWE-502: Deserialization of Untrusted Data among the most dangerous weaknesses. There is a goal to remove serialization in its current form from Java as part of Project Amber. But for now, it is better to avoid using serialization and deserialization as much as possible. If this is not an option, minimize the risks by applying serialization filters. Serialization filters scan and validate the incoming data streams. You can create Pattern-based filters consist of specific patterns defined in properties, in a config file, or on the command line. They accept or reject classes in simple scenarios; Custom filters are specified in the application’s code and implemented as patterns, methods, lambda expressions, or classes. They are applied to an individual stream or all streams in a process. Pattern-based and custom filters allow the developers to create blacklists to reject classes not allowed for deserialization or whitelists to accept only recognized and accepted classes. Serialization filters are available starting with JDK 9. In Java 17, the mechanism was enhanced with context-specific filters that can be tailored to various contexts and use cases. Apply Input Sanitation Techniques You should always be suspicious about any external input because it may contain malicious data allowing an injection attack, most common being SQL injections (when an attacker includes SQL queries into the user input data) and cross-site scripting (XSS, an injection of malicious code into websites). In addition, we now have language model prompt injections. To prevent SQL injections, Parametrize SQL queries with prepared statements that act as templates, where user input is loaded dynamically. As the parameters are already given in these prepared statements, malicious queries won’t take effect; Prefer whitelists to blacklists when validating user input. As for the XSS attacks, be sure to Use modern web frameworks with better XSS prevention practices in place; Implement output encoding for transforming data into a safe format. OWASP provides an output encoder specifically for Java applications; Avoid placing variables into dangerous contexts (directly in the script, in callback functions, etc.) even with output encoding in place; Apply HTML sanitization to convert HTML input into a safe string. OWASP recommends using DOMPurify for that purpose. Take Advantage of Java API for XML Processing (JAXP) Java offers an API for XML Processing (JAXP) that enables developers to protect their applications from XPath and other XML-related attacks. The fundamental mechanism of the API is Feature for Secure Processing (FSP), which instructs XML processors to try and process XML securely. JAXP allows developers to Set the XML processing limits to prevent malformed XML sources from excessive memory consumption; Restrict external access by defining allowed and forbidden external connections. This function can be used with resolvers and catalogs to reduce the risk of malicious external connections further. Use Strong Encryption and Password Hashing Cryptographic Failures take the fourth place in the 2025 OWASP Top 10 security risks, meaning that many developers don’t implement proper encryption mechanisms. In the worst-case scenario, user passwords or other sensitive data are stored as plain text. Sometimes, obsolete or unreliable hashing mechanisms are used, and sometimes, sensitive data is automatically decrypted upon retrieval from the database. Therefore, you should Avoid using deprecated or legacy protocols (e.g., FTP, SMTP) and cryptographic algorithms (MD5, SHA1, etc.); Prefer hashing to encryption for passwords because hashing is a one-way function that converts a password into a string that cannot be reversed. Use strong adaptive and salted hashing functions as well as recommended hashing algorithms such as Argon2id for password storage; Prefer authenticated encryption to simple encryption; Always use known and trusted encryption libraries. For instance, Spring Security offers numerous authentication and authorization features out-of-the-box, and you don’t have to use the rest of the Spring framework for development. Another alternative is to utilize a third-party solution providing authentication and authorization services, such as Auth0. Be Careful with Error and Exception Handling Error and exception messages can expose sensitive information such as configuration data and internal system components. For instance, a FileNotFoundException can reveal the file system layout, but even if the message is removed, the exception still tells whether the file exists. In addition, a stack trace may reveal information about technologies used in the development. Therefore, sanitizing exception messages and types is necessary before passing them to end users. Update the Runtime Quarterly Quarterly Critical Patch Updates (CPUs) help developers to keep the Java runtime safe by promptly introducing security patches and minimizing risks of zero-day attacks. If you don’t update the runtime regularly, your Java runtime harbors unpatched vulnerabilities making other security techniques you implement as good as useless. For example, 135 common vulnerabilities and exposures (CVEs) were identified and patched in JDK 11 within five years of its existence! What if you use free Java 6(7) builds that are no longer supported? If upgrading to a newer Java version is not an option, consider using Liberica JDK 6(7), which receives quarterly security updates and essential fixes like any other Liberica JDK version. Harden your Container Images A container is a cornerstone of cloud-native microservice development. But at the same time, it is the weakest link of a modern software supply chain. According to the Supply Chain Visibility & Risk Study by Netrise, a typical container image includes 604 known vulnerabilities on average, nearly half of them years old. The situation with Java services is particularly grave as 44% of Java services contain known-exploited vulnerabilities. Therefore, containers must be as secure as any other component of the IT infrastructure. Teams often neglect this issue, especially when they build containers on a random base image. This results in bloated container images with dozens of unused libraries, compilers, package managers, and nested with CVEs. That makes exploitation easier and the operational burden higher as teams have to spend resources on patching the code they didn’t even write. Hardened images exist to eliminate the known failure modes of bloated, mutable containers: they strip the attack surface, contain low-to-zero CVEs by design, and are signed and attestable so SREs get a stable, auditable foundation. Several vendors offer them. Pick one that owns OS and runtime security end to end, ships signed images with SBOMs and provenance, and continuously rebuilds with a published patch SLA. For Java services, consider using BellSoft Hardened Images with first-class Java and Linux support and capabilities of in-house patching. Implement A Software Bills of Materials (SBOM) A software bill of materials (SBOM) is a comprehensive inventory solution to identify vulnerabilities throughout the supply chain. SBOMs provide unmatched visibility into the project structure by listing all software components the program is made of, including direct and transitive dependencies, with essential data about them: supplier name, component name and version, present vulnerabilities, etc. As such, an SBOM enables the developers to Identify nested vulnerabilities, Minimize risks of license violations, Understand the state of dependencies (whether they are up to date or were abandoned two years ago). Conclusion: How to Stay Up-to-date with Security Measures Cybercriminals are getting bolder and craftier, but the good news is that security experts worldwide develop new techniques and measures aimed at repelling their attacks. Implementing them all may take time, so plan a step-by-step cybersecurity strategy, beginning with the most urgent tasks: Implement vulnerability scanners and generate a software bill of materials for your project to gain understanding of the current security status of your infrastructure. From here, gradually substitute vulnerable libraries with safer solutions; Transfer your cloud workloads to BellSoft Hardened Images to optimize resource consumption, eliminate licensing issues, and ensure the absence of CVEs in your project; Implement the security practices described in this document and promote development processes with security as the core value; Know cold the OWASP Top 10 security risks and recommendations on their prevention to promote safer coding practices in your team. - [Container Security Issues Continue to Befuddle Software Developers](https://bell-sw.com/news/container-security-issues-continue-to-befuddle-software-developers/): Container Security Issues Continue to Befuddle Software Developers A new survey from BellSoft found that the tools and strategies developers are using to protect their companies from container-related incidents are undermining security goals San Jose, California (January 29, 2026) BellSoft, the OpenJDK vendor delivering the most complete Java experience, announces the results of a survey on container security. A key takeaway from the survey is that, while the container ecosystem has matured significantly over the past decade, fundamental questions about security practices remain unresolved. To better understand how developers are addressing these issues, BellSoft surveyed 427 professionals at Devoxx 2025 in October. The results revealed insights into how developers select and build container images, which security practices they follow, the challenges they encounter and how current security practices fall short of helping developers achieve their stated goals. Following are the key findings from the survey: Container security incidents are becoming more common Nearly one in four respondents (23%) reported having experienced a security incident. The problem isn't detection, it's the gap between disclosure and remediation. In this window, often weeks or months, organizations operate with known exposures. The root causes range from strategy and tooling to human error 62% said human errors were the biggest contributor to container security mistakes. Developers ranked shells (54%) and package managers (39%) as the most essential tools inside the base container. Package managers present a particularly critical security concern, as they expand the attack surface both directly and by enabling runtime installation of additional unnecessary components. Combined with other non-essential tools, this creates substantial vulnerability exposure in production environments. A more practical approach is using hardened minimal runtime images, paired with fuller “debug builds” during development, allowing both security and diagnostics without compromise. 55% reported using general-purpose Linux distributions (Ubuntu/Debian or Red Hat-based systems) with hundreds of packages their applications never use. Each represents potential vulnerabilities requiring security patches. When a vulnerability emerges, security teams must evaluate impact and coordinate across thousands of instances, regardless of whether the application uses the affected package. Trusted registries (45%) and vulnerability scanning (43%) were the most commonly employed security mechanisms. These represent basic approaches to container security, whereby organizations are constantly responding to newly discovered vulnerabilities rather than building foundations to minimize exposure. While 31% said they update container images with every release and 26% do so when critical vulnerabilities emerge, 33% update monthly, rarely or only a few times yearly, creating a substantial risk to applications and organizations. Developers have a plan for solving the container security conundrum 48% said pre-hardened, security-focused base images would be most helpful in ensuring container security. Hardened vendor-maintained images directly address the root causes of today’s container security challenges, reducing vulnerability exposure, operational strain, cloud costs and risk of human errors. “Across every section of the survey, one message repeats consistently: Teams want security, efficiency and simplicity but their current strategies and tooling makes this difficult to achieve,” said Alex Belokrylov, CEO at BellSoft. “By adopting hardened images, much of the ongoing security and maintenance responsibility shifts to the image vendor, reducing operational burden and total cost of ownership, while enabling more stable, low-maintenance, and highly secure container environments” To view the complete 2025 State of Container Security report from BellSoft, go here. - [Liberica Native Image Kit 23.0.11, 23.1.10, and 25.0.2 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-11-23-1-10-and-25-0-2-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.11 for JDK 17, 23.1.10 for JDK 21, and 25.0.2 for JDK 25 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Download Liberica NIK Notable improvements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2026-21945 7.5 security-libs java.security network low none none unchanged none none high CVE-2026-21932 7.4 client-libs java.awt network low none required changed none high none CVE-2026-21933 6.1 core-libs java.net network low none required changed low low none CVE-2026-21925 4.8 core-libs java.rmi network high none none unchanged low low none CVE-2025-43368 7.5 javafx web network high none required unchanged high high high CVE-2025-7425 7.5 javafx web network high none required unchanged high high high CVE-2025-6021 5.9 javafx web network high none none unchanged none none high CVE-2025-7424 5.5 javafx web local low none required unchanged none none high CVE-2025-6052 3.7 javafx media network high none none unchanged none none low CVE-2026-21947 3.1 javafx web network high none required unchanged none low none CVE-2025-47219 3.1 javafx media network high none required unchanged low none none Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [How to Create UI in Java with Vaadin: Security Configuration, Layouts, Grid, Filter Search](https://bell-sw.com/blog/how-to-create-ui-in-java-with-vaadin-security-configuration-layouts-grid-filter-search/): Vaadin is a web framework for building full-stack web applications completely in Java, without using JavaScript or HTML. It has a great variety of ready UI components, built-in security and real-time communication, it enables Java developers to build data-rich enterprise applications without leaving the Java realm. Today, we will spin up a beautiful Vaadin UI with Spring Boot from ground zero and look at some core components so that you can continue the exploration later on your own. The code for the demo is available on GitHub. Table of Contents What We Will Build Prerequisites Add Vaadin Dependencies Create a Login View Create a Main View Create and Configure a Grid Add Filter Search Conclusion What We Will Build I’ll use my NeuroWatch project as a demo. I like to call it Petclinic on Steroids: instead of pets and owners, it displays civilians with implants and enables real-time monitoring of implant health data. It is built on Spring Boot and couples Spring Security, Kafka, and MongoDB. It gives us a perfect starting point for creating a login page, grid with filtering, an editor form secured by roles, and also a dashboard for real-time log processing. In this article, we will look into the basics such as the login form, layouts, grids, and filters. In the next article, we will master user interaction, forms with validation, and live UI updates. Prerequisites Docker and Docker Compose; Java 17 or newer. For instance, this project was built on Liberica JDK 25, a runtime recommended by Spring; Your favorite IDE. Add Vaadin Dependencies You can select the Vaadin dependency in Spring Initializr when building a new project. Alternatively, you can add Vaadin to the existing application. You will need vaadin-core, vaadin-spring-boot-starter, and a profile to build the frontend for production. com.vaadin vaadin-core com.vaadin vaadin-spring-boot-starter production true com.vaadin vaadin-core com.vaadin vaadin-dev com.vaadin vaadin-maven-plugin ${vaadin.version} frontend compile prepare-frontend build-frontend The profile creates a deployable artifact where the frontend is compiled, bundled, minified, and stripped off dev mode features such as hot reload or dev debugging. Consequently, to build the project, we will run: ./mvnw package -Pproduction Create a Login View We will have a main layout and a separate login view. First, let’s create a LoginView class. It will extend Main, which is a Vaadin component that renders as an HTML
element. Implement BeforeEnterObserver so the view can intercept navigation before the router enters it. public class LoginView extends Main implements BeforeEnterObserver { } Let’s add several useful class-level annotations. The @Route annotation registers this view at the /login route. autoLayout = false tells Vaadin not to wrap this view in your app’s normal layout (nav bars, side menus, etc.). The @PageTitle annotation sets the browser tab title to “Login” when this view is active. The @AnonymousAllowed annotation allows unauthenticated users to access this route. @Route(value = "login", autoLayout = false) @PageTitle("Login") @AnonymousAllowed public class LoginView extends Main implements BeforeEnterObserver { } After that, add three class fields: A LOGIN_PATH constant used for the form’s action; AuthenticationContext from Vaadin Spring Security, which gives you helper methods like isAuthenticated(); LoginForm, Vaadin’s ready-made form component. The out-of-the-box login form consists of a title, two input fields ("Username" and "Password"), and two buttons ("Log In" and "Forgot Password"). All of that with only creating a new LoginForm object! public static final String LOGIN_PATH = "login"; private final AuthenticationContext authenticationContext; private final LoginForm login; In the LoginView constructor, we need to inject the AuthenticationContext. Here, we will also instantiate the LoginForm. Set the form’s POST target with setAction(). On submit, the browser posts credentials to /login, which Spring Security’s formLogin filter handles. Let’s also hide the built-in “Forgot password” button for simplicity sake. The setSizeFull() method makes the root
fill the available space. ContentDiv creates a wrapper
and places the LoginForm inside it, which is useful for styling and centering the form. The add(contentDiv) method adds the
(with the login form inside) to the
component—i.e., it appears on the page. LoginView(AuthenticationContext authenticationContext) { this.authenticationContext = authenticationContext; login = new LoginForm(); login.setAction(LOGIN_PATH); login.setForgotPasswordButtonVisible(false); setSizeFull(); addClassNames("login-view"); var contentDiv = new Div(login); contentDiv.addClassNames("content-div"); add(contentDiv); } We need to override only one method, beforeEnter() that accepts the BeforeEnterEvent. It fires right before the navigation enters this view. If the user is already logged in, we will not show the login page and forward them to the root route (/). Otherwise, we Check the query string for errors. Spring Security appends this when a login attempt fails. The LoginForm.login.setError(true) flips the component into its “error” state and shows the “Invalid username or password” message. @Override public void beforeEnter(BeforeEnterEvent event) { if (authenticationContext.isAuthenticated()) { event.forwardTo(""); return; } if (event.getLocation().getQueryParameters().getParameters().containsKey("error")) { login.setError(true); } } With that, our login page with a form is ready! Create a Main View Let’s now create a main layout for our application. It extends the AppLayout that gives you a responsive shell with a top navigation bar and a left-side drawer that wraps around your actual views. The @Layout annotation marks this class as a routing layout so views can be shown inside it. In practice, other @Routed views will use this as their parent layout. MainLayout will extend AppLayout, which will give us: A top navigation bar area (Navbar), A side drawer (Drawer), and A place where the routed view content is displayed. @Layout @PermitAll public class MainLayout extends AppLayout { } We will build the UI structure in the class constructor. First, let’s create a DrawerToggle. This button opens/closes the left drawer with the vertical menu. Then, create an H1 object. It will create a header text component that says “NeuroWatch.” You can add some in-line styles to the logo to make it nicer. The addToNavbar() method adds the button and the logo into the top navigation bar area of AppLayout. That means every view using this layout will show a top bar with the hamburger and “NeuroWatch”. public MainLayout() { DrawerToggle toggle = new DrawerToggle(); H1 logo = new H1("NeuroWatch"); logo.getStyle().set("font-size", "1.5em").set("margin", "0"); addToNavbar(toggle, logo); } Next, we need to create several RouterLink objects. Each RouterLink navigates to a view class, which should be a Vaadin route, typically annotated with @Route. You can add as many links to other views as you want. They will always be available in the side drawer. Finally, add the links to the Vertical layout and add this layout to the drawer with the addToDrawer() method. public MainLayout() { DrawerToggle toggle = new DrawerToggle(); H1 logo = new H1("NeuroWatch"); logo.getStyle().set("font-size", "1.5em").set("margin", "0"); addToNavbar(toggle, logo); RouterLink civLink = new RouterLink("Civilians", CivilianView.class); RouterLink logLink = new RouterLink("Implant Logs", ImplantLogView.class); RouterLink logsLiveLink = new RouterLink("Live Logs", LiveLogsView.class); addToDrawer(new VerticalLayout(civLink, logLink, logsLiveLink)); } Create and Configure a Grid Now, it’s time to create a CivilianView to display the data about civilians. In this section, we will learn how to create A grid to display data, Filters to filter items in a grid, A dialogue window with details on each civilian in a grid, An edit form that will be visible to admins only. Let’s start with the grid. We will create a foundation now, and later, we will add additional UI elements to it. Сreate a CivilianView class that extends VerticalLayout, which is a Vaadin view that lays children top-to-bottom. Then, add the root annotation that registers this view at the root path /. layout = MainLayout.class says, “render me inside MainLayout.” This way, we get the navbar and the drawer frame. The @PermitAll annotation means that anyone who has logged in can open this view. @Route(value = "", layout = MainLayout.class) @PermitAll public class CivilianView extends VerticalLayout { } Inject CivilianService and add the Grid for Civilian with no auto-generated columns (false) as we will define them manually. Also, add the CallbackDataProvider so the grid can ask for the current page of data and the total count. A CallbackDataProvider is a Vaadin DataProvider implementation that loads data by calling your code (callbacks) whenever the UI needs it. We will build it later and use it to populate the grid with data. private final CivilianService civilianService; private final Grid grid = new Grid<>(Civilian.class, false); private CallbackDataProvider dataProvider; In the constructor, we will build the DataProvider, set up the grid and add the Lumo utility classes. Lumo is Vaadin’s utility for CSS. For me, the name bears a strong resemblance to Lums from Rayman Legends, somehow. Anyway, we can add these utility classes to the root VerticalLayout so it looks how we want without writing custom CSS. For instance, let’s add these classes with the addClassNames() method: LumoUtility.BoxSizing.BORDER sets box-sizing: border-box. It means that padding and borders will be included in the component’s declared width/height. LumoUtility.Display.FLEX sets display: flex, turning the layout into a flex container. LumoUtility.FlexDirection.COLUMN sets flex-direction: column, which means that children will stack vertically. Filters, when we add them, will be the row on top, and grid will be below. LumoUtility.Padding.MEDIUM adds medium padding around the container. LumoUtility.Gap.SMALL adds a small gap between direct children. In our case, it is a space between the filters bar and the grid. LumoUtility.FlexWrap.WRAP sets flex-wrap: wrap. It means that if the container’s children can’t fit in one row/column, they’ll wrap instead of overflowing. public CivilianView(CivilianService civilianService) { this.civilianService = civilianService; addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL, LumoUtility.FlexWrap.WRAP); } Now, we need to build the CallBackProvider. I suggest we transfer this logic into a separate method: public CivilianView(CivilianService civilianService) { dataProvider = buildProvider(); } private CallbackDataProvider buildProvider() { } CallbackDataProvider uses two functional interfaces, FetchCallback and CountCallback. Consequently, it has two core responsibilities: FetchCallback returns a Stream of items for the current visible page. CountCallback returns the total number of items so the grid can size the scrollbar and paging. Vaadin calls these repeatedly as the user scrolls, sorts, filters, etc. We need to create a provider with DataProvider.fromCallbacks(fetchCallback, countCallback). For the FetchCallback, we call the serviceFetch() method, returning the collection of civilians, stream the collection, and return a Stream. For the CountCallback, we return a total number of rows so the grid can paginate. private CallbackDataProvider buildProvider() { return DataProvider.fromCallbacks( /* fetch callback */ query -> { int offset = query.getOffset(); int limit = query.getLimit(); return serviceFetch(offset, limit).stream(); }, /* count callback */ _ -> (int) serviceCount() ); } private Collection serviceFetch(int offset, int limit) { return civilianService.getCivilians(offset, limit); } private long serviceCount() { return civilianService.countCivilians(); } The DataProvider is ready, the next step is to configure the grid. Let’s do that in a separate method as well: public CivilianView(CivilianService civilianService) { configureGrid(); } Now, to the actual grid configuration. First, let’s connect the grid to its data source. Bind our DataProvider with a setItems() method. This way, we are telling the grid to not use a fixed list but fetch rows from the provider. Our provider is a CallbackDataProvider, which means that the grid will call it as the user scrolls the page, asking for slices of data and total count. grid.setItems(dataProvider); Then, add two columns, where the values are read via the method references. The first column will display a national id, header text “National ID.” The second column will display a legal name, header text “Legal Name.” grid.addColumn(Civilian::getNationalId).setHeader("National ID"); grid.addColumn(Civilian::getLegalName).setHeader("Legal Name"); With the addThemeVariants() method, we will enable the theme variant LUMO_WRAP_CELL_CONTENT. This way, a long cell text will wrap instead of overflowing. The setSizeFull() method makes the layout, i.e., the view itself, fill the available space. grid.addThemeVariants(GridVariant.LUMO_WRAP_CELL_CONTENT); Put the grid into view with the add() method in the class constructor: public CivilianView(CivilianService civilianService) { this.civilianService = civilianService; dataProvider = buildProvider(); configureGrid(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL, LumoUtility.FlexWrap.WRAP); add(grid); } Run the application. After you login, you will see a grid with civilians and a side drawer with links to other pages. Add Filter Search Right now, we display all civilians. But in most cases, we need some sort of filtering. First, let’s add some new UI elements. We will have three IntegerField fields for filtering by lot numbers, and also a TextField to search for a civilian by national ID. Let’s also add a button that will clear all filter fields when clicked. @Route(value = "", layout = MainLayout.class) @PermitAll public class CivilianView extends VerticalLayout { private final IntegerField lotGte = new IntegerField("Lot ≥"); private final IntegerField lotLte = new IntegerField("Lot ≤"); private final IntegerField lotN = new IntegerField("Lot #"); private final TextField nationalId = new TextField("National ID"); private final Button clear = new Button("Clear"); } We will initialize the filters in the constructor, but configure them in a separate method. Also in the constructor, we will create a new HorizontalLayout and add the filters and the button there so that they are displayed in the horizontal row. The setDefaultVerticalComponentAlignment(Alignment.END) aligns child components so their ends line up. This way, we align them with the Grid header. We should also add the filters to view with the add() method. In this case, they will be displayed over the grid. public CivilianView(CivilianService civilianService) { configureFilters(); HorizontalLayout filters = new HorizontalLayout(lotGte, lotLte, lotN, nationalId, clear); filters.setDefaultVerticalComponentAlignment(Alignment.END); add(filters, grid); } Let’s move on to configuring the filters. First, we need to add a listener. HasValue.ValueChangeListener creates one generic listener that calls the refresh() method whenever any filter value changes. The wild generics enable the listener to handle value-change events from different field types, like IntegerField, TextField, etc. private void configureFilters() { HasValue.ValueChangeListener> listener = _ -> refresh(); } Next, let’s subscribe that listener to each filter field. As a result, changing any filter triggers the refresh() method which we will define as well. Let’s also add a clickListener to the Clear button that will trigger clearing all filter fields. Clearing triggers value-change events too, so we will get a refresh automatically. We don’t need to explicitly call refresh() here, it’ll happen via the listeners as each field clears. private void configureFilters() { HasValue.ValueChangeListener> listener = _ -> refresh(); lotGte.addValueChangeListener(listener); lotLte.addValueChangeListener(listener); lotN.addValueChangeListener(listener); nationalId.addValueChangeListener(listener); clear.addClickListener(_ -> { lotGte.clear(); lotLte.clear(); lotN.clear(); nationalId.clear(); }); } The refresh() method is super simple: we just call an in-build refreshAll() method of the DataProvider to update the provider when filters are applied or cleared. The provider will call the fetch/count again, and we will get new rows based on the filter values private void refresh() { dataProvider.refreshAll(); } We remember that the buildProvider() method uses the serviceFetch() method to retrieve data. We need to improve it to add filtered search so that it decides which backend method to call based on the current filter values. By our logic, only one filter will be applied at a time. So, we check whether a filter field has value, fetch civilians and return. Because the method uses an if-chain priority, a user can set multiple fields, but only one applies, namely, the first match. private Collection serviceFetch(int offset, int limit) { if (!lotN.isEmpty()) { return civilianService.getCiviliansByLotNumber(offset, limit, lotN.getValue()); } if (!lotGte.isEmpty()) { return civilianService.getCiviliansByLotNumberGreaterOrEqual(offset, limit, lotGte.getValue()); } if (!lotLte.isEmpty()) { return civilianService.getCiviliansByLotNumberLessOrEqual(offset, limit, lotLte.getValue()); } if (!nationalId.isEmpty()) { return civilianService.getCiviliansByNationalId(offset, limit, nationalId.getValue()); } return civilianService.getCivilians(offset, limit); } We should also update the serviceCount() method accordingly: private long serviceCount() { if (!lotN.isEmpty()) { return civilianService.countByLotNumber(lotN.getValue()); } if (!lotGte.isEmpty()) { return civilianService.countByLotNumberGreaterOrEqual(lotGte.getValue()); } if (!lotLte.isEmpty()) { return civilianService.countByLotNumberLessOrEqual(lotLte.getValue()); } if (!nationalId.isEmpty()) { return civilianService.countByNationalId(nationalId.getValue()); } return civilianService.countCivilians(); } If you want to implement combined filtering, you need to change the serviceFetch() method to compute a single predicate and query with it. Great, our CivilianView has filters now. You can search for a civilian by national ID or select all civilians who have implants with a lot number greater or lesser than or equal to a specified value. Conclusion In this article, we explored the core Vaadin UI components you need for a real app: locking things down with security rules, creating a reusable main layout with navigation, building a Grid with explicit columns, and wiring up filter inputs so the data refreshes smoothly. The nice part is that layouts, routing, components, and data access patterns fit together naturally, and you can stay in Java end-to-end while still shipping a modern UI. Next up, we’ll cover more Vaadin components you’ll use in real applications, like forms with validation, dialogs, tabs, notifications, and richer data handling patterns. - [How to Create UI in Java with Vaadin: Dialog, Forms with Validation, Real-Time Updates](https://bell-sw.com/blog/how-to-create-ui-in-java-with-vaadin-dialog-forms-with-validation-real-time-updates/): This is a second article in the series of building UI in Java with Vaadin. If you read the first part of this series, you already have a solid Vaadin-based frontend skeleton with Login form, A reusable main layout with navigation, A grid that can page through data efficiently using a CallbackDataProvider,Filter fields so the UI refreshes immediately when search criteria changes. In the second part, we’re taking that foundation and building on it. We’ll Add dialogs for displaying data in an overlay, Build forms with validation and binders so that only admins can change civilian data, and Introduce real-time updates so your UI can react instantly when data changes. Table of Contents Prerequisites and Project Setup Add a Dialog Window Add an Edit Form with a Binder Enable Real-Time Updates Conclusion Prerequisites and Project Setup The demo project we are building is available on GitHub. A quick reminder: the application is called NeuroWatch, it displays civilians with implants and enables real-time monitoring of implant health data. The application uses Spring Security, Kafka, MongoDB, and Vaadin. Add a Dialog Window The grid currently displays only national IDs and civilians’ names. Let’s add a dialog window that will display detailed info about the civilian in an overlay. First, we need to add a listener to the grid so that when the row is selected, a details dialog opens. Inside the configureGrid() method, add a listener to the grid via asSingleSelect() method, which puts the grid in single-row selection mode. The value change listener fires whenever the selection changes. If a civilian was selected, we call the openCivilianDialog(civilian) method. private void configureGrid() { grid.asSingleSelect().addValueChangeListener(e -> { Optional.ofNullable(e.getValue()).ifPresent(this::openCivilianDialog); }); } Next, let’s write the openCivilianDialog() method. As we want the dialog to be built anew each time a civilian is selected, we create a Dialog object in this method. You can also do some styling, like setting a header and window width: private void openCivilianDialog(Civilian civilian) { Dialog dialog = new Dialog(); dialog.setHeaderTitle(civilian.getLegalName()); dialog.setWidth("48rem"); } Now, let’s create a tab called “Details” and add it to the Tabs. I know, using Tabs with just one tab might look strange, but it scales really well when we add “Edit”, “Add Implant”, etc. Tab detailsTab = new Tab("Details"); Tabs tabs = new Tabs(detailsTab); Then, we create the details panel component (we will write the logic in a separate method in just a few moments) and map a tab to a page via the Map map. The map ties each tab to its corresponding content component. With multiple tabs, it becomes a control center for switching tabs. Component detailsPanel = buildDetailsPanel(civilian); Map map = Map.of( detailsTab, detailsPanel ); After that, we need to place all pages into a single Div container. You can style it as well, for instance, set the container position. We then set up visibility by first hiding all tabs and then displaying the default one, which is the details panel. Div pages = new Div(detailsPanel); pages.getStyle().set("position", "relative"); map.values().forEach(p -> p.setVisible(false)); detailsPanel.setVisible(true); Now, let’s adjust the tab switching logic. Add a SelectedChangeListener to the tabs so that whenever the user clicks a tab, we hide all pages and show only the page associated with the selected tab. tabs.addSelectedChangeListener(e -> { map.values().forEach(p -> p.setVisible(false)); map.get(tabs.getSelectedTab()).setVisible(true); }); Finally, add tabs with pages to the dialog, and also add a “Close” button to the footer. With dialog.open() we open the dialog window. dialog.add(tabs, pages); dialog.getFooter().add(new Button("Close", ev -> dialog.close())); dialog.open(); Great, the next step is to build the actual UI shown in the Details tab. First, let’s create a FormLayout object, which is an optimal choice for label/value pairs. After that, we can add all required fields with the addFormItem() method. Note that we are using span because we have read-only values. private Component buildDetailsPanel(Civilian civilian) { /* CIVILIAN meta */ FormLayout civForm = new FormLayout(); civForm.addFormItem(new Span(civilian.getNationalId()), "National ID"); civForm.addFormItem(new Span(civilian.getBirthDate().toString()), "Birth date"); civForm.addFormItem(new Span(civilian.isCriminalRecord() ? "Yes" : "No"), "Criminal record"); civForm.addFormItem(new Span(civilian.isUnderSurveillance() ? "Yes" : "No"), "Under surveillance"); } Civilians also have implants, the information about which we would also like to display. I suggest we use a nested grid for implant data for a smoother look. We already know how to create and configure a grid from the previous article. What is worth mentioning here is that we populate the grid with implants already available as a collection in Civilian. We can also constrain the grid height so that it becomes a small scrollable area instead of expanding the dialog to the max. /* IMPLANT list */ Grid implantGrid = new Grid<>(Implant.class, false); implantGrid.addColumn(Implant::getType).setHeader("Type").setAutoWidth(true).setFlexGrow(1); implantGrid.addColumn(Implant::getModel).setHeader("Model").setAutoWidth(true).setFlexGrow(1); implantGrid.addColumn(Implant::getVersion).setHeader("Ver").setAutoWidth(true).setFlexGrow(1); implantGrid.addColumn(Implant::getManufacturer).setHeader("Made by").setAutoWidth(true).setFlexGrow(1); implantGrid.addColumn(Implant::getSerialNumber).setHeader("Serial #").setAutoWidth(true).setFlexGrow(1); implantGrid.addColumn(Implant::getLotNumber).setHeader("Lot #").setAutoWidth(true).setFlexGrow(1); implantGrid.addColumn(Implant::getInstalledAt).setHeader("Installed").setAutoWidth(true).setFlexGrow(1); implantGrid.setItems(civilian.getImplants()); implantGrid.setHeight("200px"); // small scroll area implantGrid.addThemeVariants(GridVariant.LUMO_WRAP_CELL_CONTENT); Finally, let’s assemble everything in one vertical layout. Metadata form goes on top, the follows the “Implants” heading, and then the implant grid. Padding/spacing disabled to keep the dialog compact and visually tight. VerticalLayout content = new VerticalLayout(civForm, new H5("Implants"), implantGrid); content.setPadding(false); content.setSpacing(false); content.setSizeFull(); return content; As a result, we now have the following structure: The grid acts as a master list, Selecting an item opens a details dialog, The dialog uses scalable Tabs and page switching pattern, The details view combines a FormLayout for metadata and a Grid for related items This sets us up perfectly for the next step: adding extra tabs for editing. Add an Edit Form with a Binder Let's add another tab to our dialog window that lets users with the ADMIN role edit civilians. First, let’s create a hasRole() method that accepts the required role and uses SecurityContextHolder to verify whether the user has this role. private boolean hasRole(String role) { return SecurityContextHolder.getContext().getAuthentication() .getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals(role)); } Note that this is a UI-level check. It hides or shows UI features, but we still need service/repository security enforcement, because UI checks don’t stop someone from calling the endpoints directly. Now, add a new Tab editTab to the openCivilianDialog() method. Use the hasRole() method to check whether the user is admin. If yes, add the new tab to the tabs. Create the editPanel Component, add it to the map together with the editTab, and to the Div. We will build this component in a separate method buildEditForm(). private void openCivilianDialog(Civilian civilian) { Tab editTab = new Tab("Edit"); Tabs tabs = new Tabs(detailsTab); if (hasRole("ROLE_ADMIN")) tabs.add(editTab); Component editPanel = buildEditForm(civilian, dialog); Map map = Map.of( detailsTab, detailsPanel, editTab, editPanel ); Div pages = new Div(detailsPanel, editPanel); Here, we create a TextField and two CheckBoxes initialized with the current values for the civilian data we can edit. private Component buildEditForm(Civilian civilian, Dialog dialog) { TextField name = new TextField("Legal name"); Checkbox criminal = new Checkbox("Criminal record", civilian.isCriminalRecord()); Checkbox surveil = new Checkbox("Under surveillance", civilian.isUnderSurveillance()); } Then, we create a Vaadin Binder for the Civilian bean type. Binder is Vaadin’s way to connect UI fields to a Java object: The getter tells Binder how to read the current value from the object. The setter tells Binder how to write the edited value back. BeanValidationBinder supports Java Bean Validation, so you don’t have to add the validation manually to the UI layer. Using the bind method of the binder, we bind the text field and checkboxes to the relevant civilian values. The readBean() method reads values from the civilian bean into the bound fields and sets the initial UI state. Binder binder = new BeanValidationBinder<>(Civilian.class); binder.bind(name, Civilian::getLegalName, Civilian::setLegalName); binder.bind(criminal, Civilian::isCriminalRecord, Civilian::setCriminalRecord); binder.bind(surveil, Civilian::isUnderSurveillance, Civilian::setUnderSurveillance); binder.readBean(civilian); Let’s also add the save Button with a ClickListener. On save, we call: The writeBeanIfValid(civilian) method of the binder that validates the bound fields. If valid, the values are written from the UI back into the civilian object; civilianService.updateCivilian(civilian) to persist changes; refresh() so the grid updates; close() to close the dialog. Let’s also add the cancel Button to close the dialog window without persisting any changes. Button save = new Button("Save", e -> { if (binder.writeBeanIfValid(civilian)) { civilianService.updateCivilian(civilian); refresh(); dialog.close(); } }); Button cancel = new Button("Cancel", e -> dialog.close()); And finally, we return a vertical stack containing the input fields and two buttons. The caller will add this to the dialog. return new VerticalLayout(name, criminal, surveil, save, cancel); Now, if you run the app and login as an admin, you will see an edit tab in the civilian dialog. You can change the values, and the updated data will be immediately displayed in the grid. Enable Real-Time Updates The demo application uses Kafka to receive and process a stream of implant monitoring logs. So, in this section, we will build a live-updating grid in Vaadin so the UI can update in real-time as the logs come in. Create a LiveLogsView class. The only novelty in configuration is the implementation of AfterNavigationObserver. It lets the view run code after navigation completes, which is a good moment to start listening to live data. @Route(value = "live-logs", layout = MainLayout.class) @PermitAll public class LiveLogsView extends VerticalLayout implements AfterNavigationObserver { } We need to add several instance fields: LiveLogBus is a service class that serves as a source of live logs; LinkedList will hold the logs currently displayed; ListDataProvider is a Vaadin provider that serves items from an in-memory collection. We will wrap the list with Collections.synchronizedList(...) so basic operations are thread-safe, because logs arrive from another thread. Grid will show the log rows. Checkbox will control whether the grid jumps to the newest row automatically. Disposable is the reactor.core class that will hold the live stream subscription so we can dispose of it when leaving the view. private final LiveLogBus bus; private final LinkedList buffer = new LinkedList<>(); private final ListDataProvider data = new ListDataProvider<>(Collections.synchronizedList(buffer)); private final Grid grid = new Grid<>(ImplantMonitoringLog.class, false); private final Checkbox autoScroll = new Checkbox("Auto-scroll", true); private Disposable subscription; Then, we need to build the UI: configure the toolbar, the grid. Nothing new here, so I’ll skip the explanation. For instructions on configuring the grid, refer to the previous article. public LiveLogsView(LiveLogBus bus) { this.bus = bus; setSizeFull(); configureGrid(); add(buildToolbar(), grid); expand(grid); } private Component buildToolbar() { HorizontalLayout bar = new HorizontalLayout(autoScroll); bar.setAlignItems(Alignment.CENTER); bar.setWidthFull(); bar.setJustifyContentMode(JustifyContentMode.START); return bar; } private void configureGrid() { grid.setDataProvider(data); grid.addColumn(ImplantMonitoringLog::getTimestamp).setHeader("Time").setAutoWidth(true); grid.addColumn(ImplantMonitoringLog::getImplantSerialNumber).setHeader("Serial").setAutoWidth(true); grid.addColumn(ImplantMonitoringLog::getCivilianNationalId).setHeader("National ID").setAutoWidth(true); grid.addColumn(l -> String.format("%.1f µW", l.getPowerUsageUw())).setHeader("Power").setAutoWidth(true); grid.addColumn(l -> String.format("%.1f %%", l.getCpuUsagePct())).setHeader("CPU").setAutoWidth(true); grid.addColumn(l -> String.format("%.2f ms", l.getNeuralLatencyMs())).setHeader("Latency").setAutoWidth(true); grid.addColumn(l -> l.getLocation() != null ? (l.getLocation().getY() + ", " + l.getLocation().getX()) : "") .setHeader("Lat, Lon").setAutoWidth(true); grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_WRAP_CELL_CONTENT); grid.setHeightFull(); } The most interesting things happen in the afterNavigation() method that we must override. First and foremost, we must subscribe to the stream of events emitted by LiveLogBus when the view becomes active with bus.stream().subscribe(...). Within the subscribe() method, update the UI safely for each new log: getUI().ifPresent(...) ensures the view is attached to a UI. ui.access(...) runs code inside Vaadin’s UI lock from a background thread. This bit is super important because without ui.access, updating Vaadin components from a non-UI thread can lead to unpredictable behavior. @Override public void afterNavigation(AfterNavigationEvent afterNavigationEvent) { // subscribe when the view becomes active subscription = bus.stream().subscribe(log -> getUI().ifPresent(ui -> ui.access(() -> { ... })) ); } Then, we Add the log to the front of the list with addFirst() so the newest one is at the top; Trim the list to max 5000 rows to avoid unbounded memory growth; Update the grid with data.refreshAll(); If auto-scroll is on, scroll to row 0, which is the newest one. @Override public void afterNavigation(AfterNavigationEvent afterNavigationEvent) { subscription = bus.stream().subscribe(log -> getUI().ifPresent(ui -> ui.access(() -> { buffer.addFirst(log); // trim to avoid unbounded growth (keep last 5000 rows) if (buffer.size() > 5000) buffer.removeLast(); data.refreshAll(); if (autoScroll.getValue() && !buffer.isEmpty()) { grid.scrollToIndex(0); } })) ); } When the user navigates away or closes the tab, the view should be detached. We can define this logic in the onDetach() method that we also override. Here, we dispose of the subscription so the view stops consuming events, which prevent memory leaks or subscribers piling up. @Override public void onDetach(DetachEvent detachEvent) { if (subscription != null) { subscription.dispose(); subscription = null; } } Finally, one more extremely important thing is to enable Vaadin server push with the @Push annotation added to a class extending the AppShellConfigurator. This functionality allows ui.access(...) updates to be sent to the browser automatically. Without push, the server could update the component tree, but the browser wouldn’t see it until the next client request such as a click. @SpringBootApplication @Push(PushMode.AUTOMATIC) public class NeuroWatchApplication implements AppShellConfigurator { public static void main(String[] args) { SpringApplication.run(NeuroWatchApplication.class, args); } } Conclusion We have built a beautiful UI with dialogs, edit forms, and a live monitoring console. Vaadin makes this kind of thing feel straightforward. You stay in Java, you keep your UI and backend logic in the same mental model, and you still get modern UX patterns like dialogs, tabs, and real-time updates with clean, predictable code. If you want more tutorials like this, subscribe to our newsletter so as not to miss them! - [Build AI Agents in Java with Embabel: Step-by-Step Guide](https://bell-sw.com/blog/build-ai-agents-in-java-with-embabel-step-by-step-guide/): Embabel is an agent framework on JVM that mixes LLM and domain models and enables you to integrate sophisticated agentic flows into your application guarded by strong typing and your own code. If you want reliable AI integration into your enterprise application, Embabel is worth looking at. This article explores what Embabel is, how it is different from other frameworks, and how to integrate Embabel into an existing application, from dependencies to the complete agentic workflow. The code is available on GitHub. Table of Contents What is Embabel? Embabel vs Spring AI How to Integrate Embabel into a Java App What We Will Build Add the Dependencies Create the First Action Build a Multi-Action Workflow Conclusion What is Embabel? Embabel lets you build agentic workflows based on the concept of goals, actions, and conditions. The highest level is the goal, which is the result the agent is trying to achieve on behalf of the user. The goal is defined by the developer in a method annotated with @Goal. To achieve this goal, an agent uses actions. Actions are defined in methods annotated with @Action. Some actions can use LLM, some are deterministic steps like database lookups, scoring, validation, or calling services. An agent plans the workflow to achieve the goal, but it does so independently of the developer. A plan is formulated dynamically and reassessed after each action. There are also conditions that the agent observes during the planning. This architecture is what makes Ebabel an optimal choice for enterprise JVM applications that want to benefit from Agentic AI. You get Sophisticated planning. It adds a real planning step using a non-LLM algorithm. Extensibility. You can add new actions/domain objects without rewriting the existing code. Strong typing and object orientation. Prompts and code interact through domain objects. LLM mixing. You can easily combine models for cost, privacy, or performance tradeoffs, and Embabel will pick the right one for the task and requirements. In addition, Embabel is designed on Spring and JVM, which lets it benefit from the existing enterprise features, and be testable end-to-end. Embabel vs Spring AI At this point, you may have a logical question: why not just use Spring AI? How does Embabel differ from Spring AI? The thing is that Embabel and Spring AI are different layers of the stack. Spring AI is primarily an AI integration framework for Spring apps, which focuses on giving you portable abstractions for models, vector stores, tools/function calling, etc. Embabel, on the other hand, is a higher level API that can use these underlying capabilities but adds a higher-level execution model for goal-driven workflows. You define actions over domain objects and Embabel’s platform can plan sequences of actions using a GOAP (Goal-Oriented Action Planning) planner, rather than you hard-coding a single fixed chain. In practice, Spring AI helps you talk to models and wire AI components into your app. Embabel helps you build agents that decide which actions to run to reach a goal based on the input/output types and replan on the fly. How to Integrate Embabel into a Java App What We Will Build We will take a cyberpunk-themed demo as a baseline. It stores civilians with their implants, collects monitoring stats from the implants, and searches for monitoring logs based on location and time window. What we want to do is integrate the implant incident triage workflow. Someone reports unusual telemetry in a place and time window, and the system turns that into a full incident case with risk level, affected devices, likely cause, and a detailed containment plan. Prerequisites Spring Boot 3.x. At the time of writing this article. Embabel doesn’t yet support Spring boot 4.x. Java 17+. This demo was built using Liberica JDK 25 recommended by Spring. Docker and Docker Compose for spinning up the database instance in a container. Your favorite IDE. Add the Dependencies Let’s start with adding the necessary dependencies. We will use Ollama for this demo, but you can use Embabel with different LLMs or even mix them. So, we will need an embabel-ollama-starter. Also, we need an embabel-agent-starter-shell and a spring-shell-starter as we will run the app in a shell. You can also use the Embabel’s MCP starter or basic starter. 25 0.3.4-SNAPSHOT 3.4.0 com.embabel.agent embabel-agent-starter-ollama ${embabel-agent.version} com.embabel.agent embabel-agent-starter-shell ${embabel-agent.version} org.springframework.shell spring-shell-starter As we will run the app via shell, we need to disable spring web application and enable interactive mode of Spring Shell. You can also configure Embabel in application.properties. For instance, add the default model it will use. spring.main.web-application-type=none spring.shell.interactive.enabled=true embabel.models.default-llm=llama3.1:8b Create the First Action Let’s start with something basic. Right now, we are not showcasing Embabel’s power, but rather, getting to know the tool. We will enhance this logic later on. We will add a single action at first. The agent will receive the user input and provide an IncidentAssessment object as an output. For that, it will parse the input into an IncidentSignal object, find all affected implants in the database, and calculate the risk level based on the written logic. public record IncidentSignal( @NotNull double longitude, @NotNull double latitude, @Positive double radiusMeters, @NotNull @Past LocalDateTime from, @NotNull @Past LocalDateTime to, @NotNull @NotEmpty String metric, @Positive double threshold ) { } public enum RiskLevel { LOW, MEDIUM, HIGH, CRITICAL } public record IncidentAssessment( IncidentSignal signal, int numberOfLogs, RiskLevel riskLevel ) { } The only LLM related task in this case is to parse the user input. The logic for the database lookup and the risk assessment is deterministically defined in the application code. The output of agent work is a Java object, RiskAssessment, which provides more reliability to the agent’s response. Let’s create a class called IncidentTriageAgent. The @Agent annotation marks this class as an Embabel agent component. In practice, it’s also a Spring bean. The description is metadata used for discovery, documentation, and potentially for planning or selection when multiple agents exist. So, Embabel knows that this agent can investigate telemetry anomalies. @Agent(description = "Investigates and assesses implant telemetry anomalies in a geo/time window using MongoDB logs") public class IncidentTriageAgent { } Next, we inject ImplantMonitoringLogService. It is the “tool” here, but not an LLM tool. Rather, it’s the domain service that queries MongoDB. Embabel is ok with actions calling a normal application service. private final ImplantMonitoringLogService logService; public IncidentTriageAgent(ImplantMonitoringLogService logService) { this.logService = logService; } The action is one method that achieves a goal. So, let’s create the triageIncidentSignal() method that returns IncidentAssessment — the structured output of this action. Two annotations are important here: @Action, which means that this method is an executable step in Embabel’s world. @AchievesGoal, which means that this action is also considered a goal-completing action. It means that producing an IncidentAssessment is “done” for the described goal. Parameters: UserInput, which is the raw user message (from CLI, chat, etc.). OperationContext, which is the Embabel’s runtime context. This is how you access AI features. @AchievesGoal(description = "Parses an incident signal and assigns a risk level") @Action public IncidentAssessment triageIncidentSignal(UserInput input, OperationContext context) { } In this method, we first use LLM to parse the input into the IncidentSignal object by calling context.ai().withDefaultLlm().createObject().formatted(). What is it doing? It sends the prompt and user message to the default LLM configured for the app. It asks the model to output something that can be deserialized into the IncidentSignal object. IncidentSignal signal = context.ai().withDefaultLlm().createObject( """ Extract an IncidentSignal from the user's message. Output rules: - lon is a number in [-180, 180] - lat is a number in [-90, 90] - radiusMeters is a number in meters - from/to are ISO-8601 LocalDateTime (e.g. 2026-02-02T02:00:00) - metric is one of: neuralLatencyMs, cpuUsagePct, powerUsageUw - threshold is a finite number User message: %s """.formatted(input.getContent()), IncidentSignal.class); As a result, instead of getting back an unstructured blob of text that you then regex, you get a typed object with fields like: longitude, latitude radiusMeters from, to (as LocalDateTime per your rules) metric (must be one of the allowed strings) threshold (number) We are basically telling LLM to behave like a parser. Then, we deterministically query MongoDB logs using our domain service. Map> logs = logService.findLogsByAreaAndTime( toSpringPoint(signal.longitude(), signal.latitude()), signal.radiusMeters(), signal.from(), signal.to()); Then, we classify the risk, also deterministically. RiskLevel risk = classifyRisk(logs, signal); After that, we build the IncidentAssessment object based on the data we retrieved and calculated and return to the user. return new IncidentAssessment(signal, logs.size(), risk); The whole implementation: @Agent(description = "Investigates and assesses implant telemetry anomalies in a geo/time window using MongoDB logs") public class IncidentTriageAgent { private final ImplantMonitoringLogService logService; public IncidentTriageAgent(ImplantMonitoringLogService logService) { this.logService = logService; } @AchievesGoal(description = "Parses an incident signal and assigns a risk level") @Action public IncidentAssessment triageIncidentSignal(UserInput input, OperationContext context) { IncidentSignal signal = context.ai().withDefaultLlm().createObject( """ Extract an IncidentSignal from the user's message. Output rules: - lon is a number in [-180, 180] - lat is a number in [-90, 90] - radiusMeters is a number in meters - from/to are ISO-8601 LocalDateTime (e.g. 2026-02-02T02:00:00) - metric is one of: neuralLatencyMs, cpuUsagePct, powerUsageUw - threshold is a finite number User message: %s """.formatted(input.getContent()), IncidentSignal.class); Map> logs = logService.findLogsByAreaAndTime( toSpringPoint(signal.longitude(), signal.latitude()), signal.radiusMeters(), signal.from(), signal.to()); RiskLevel risk = classifyRisk(logs, signal); return new IncidentAssessment(signal, logs.size(), risk); } private static RiskLevel classifyRisk( Map> logs, IncidentSignal signal) { if (logs.isEmpty()) return RiskLevel.LOW; long distinctImplants = logs.size(); long exceedCount = logs.values() .stream() .flatMap(List::stream) .mapToDouble(log -> getMetricValue(log, signal.metric())) .filter(value -> value >= signal.threshold()) .count(); if (exceedCount >= 60 && distinctImplants >= 5) return RiskLevel.CRITICAL; if (exceedCount >= 30 && distinctImplants >= 3) return RiskLevel.HIGH; if (exceedCount >= 10) return RiskLevel.MEDIUM; return RiskLevel.LOW; } private static double getMetricValue(ImplantMonitoringLog log, String metric) { return switch (metric) { case "neuralLatencyMs" -> log.getNeuralLatencyMs(); case "cpuUsagePct" -> log.getCpuUsagePct(); case "powerUsageUw" -> log.getPowerUsageUw(); default -> throw new IllegalArgumentException("Unsupported metric: " + metric); }; } private static Point toSpringPoint(double lon, double lat) { return new Point(lon, lat); } } The code works as desired if we run it, but this is too easy! Right now, we are using Embabel as a fancy JSON parser, which is an overkill. If we want to truly see its power, which is goal-driven planning across multiple typed steps with tool boundaries and adaptive replanning, we must give it something to work with. For that, we should turn the current one-step triage into a multi-action incident workflow where the agent chooses what to do next based on what it learns. Build a Multi-Action Workflow Let’s enrich our incident domain model and add some new classes such as RootCauseHypothesis, EstimatedBlastRadius, ContainmentPlan, IncidentCase, and some others. I won’t paste their implementations here so as not to turn the article into a paper roll. You can study them in the repo. Now, let’s turn our agent into a pipeline that takes an incident report and turns it into a complete incident case. The goal will be to produce an IncidentCase object with the initial incident signal, assessment, list of affected implants, blast radius, hypothesis as to what might have caused the incident, and a containment plan with detailed steps. Step 1: Parse the User Message into Structured Data (LLM-Driven Step) The parseIncidentSignal() method will be our entry point. @Action(description = "Parse user's message into an IncidentSignal") public IncidentSignal parseIncidentSignal(UserInput input, OperationContext context) { } It takes raw text from the user, like “lat/lon, radius, time window, metric, threshold”. Then, it calls the default LLM and says: “Extract an IncidentSignal object, and follow these rules.” Those rules are important because they constrain the output and increase the reliability of parsing: Valid latitude and longitude ranges; ISO LocalDateTime format. LocalDateTime is alright for a small demo, but it is recommended to use other formats with databases such as timestamps with UTC or zoned times. The metric must be one of three allowed values; The threshold must be a real number. @Action(description = "Parse user's message into an IncidentSignal") public IncidentSignal parseIncidentSignal(UserInput input, OperationContext context) { return context.ai().withDefaultLlm().createObject( """ Extract an IncidentSignal from the user's message. Output rules: - lon is a number in [-180, 180] - lat is a number in [-90, 90] - radiusMeters is a number in meters - from/to are ISO-8601 LocalDateTime (e.g. 2026-02-02T02:00:00) - metric is one of: neuralLatencyMs, cpuUsagePct, powerUsageUw - threshold is a finite number User message: %s """.formatted(input.getContent()), IncidentSignal.class); } As a result, we go from the unstructured text to a typed object, IncidentSignal. Step 2: Pull the Logs and Compute a Risk Level The next action, the triageIncident() method, takes that IncidentSignal and determines the risk level. @Action(description = "Classify risk level for a signal using logs") public IncidentAssessment triageIncident(IncidentSignal signal) { Map> logs = extractLogs(signal); RiskLevel risk = classifyRisk(logs, signal); return new IncidentAssessment(signal, logs.size(), risk); } First, it calls the extractLogs() method, which queries MongoDB logs using: location (lon/lat converted to a Spring Point); radius in meters; from/to timestamps. private Map> extractLogs(IncidentSignal signal) { return logService.findLogsByAreaAndTime( toSpringPoint(signal.longitude(), signal.latitude()), signal.radiusMeters(), signal.from(), signal.to()); } That comes back as a map, where key is the implant serial number, and value is a list of logs for that implant. Then, we classify risk in the classifyRisk() method. Here, as well, we don’t use LLM, only pure Java. We count: how many implants are involved; how many log entries exceed the threshold for the chosen metric. Then, we apply deterministic thresholds: CRITICAL for lots of exceedances and many implants; HIGH for a smaller list but still spread; MEDIUM for at least 10 exceedances; Otherwise, LOW. private static RiskLevel classifyRisk( Map> logs, IncidentSignal signal) { if (logs.isEmpty()) return RiskLevel.LOW; long distinctImplants = logs.size(); long exceedCount = logs.values() .stream() .flatMap(List::stream) .mapToDouble(log -> getMetricValue(log, signal.metric())) .filter(value -> value >= signal.threshold()) .count(); if (exceedCount >= 60 && distinctImplants >= 5) return RiskLevel.CRITICAL; if (exceedCount >= 30 && distinctImplants >= 3) return RiskLevel.HIGH; if (exceedCount >= 10) return RiskLevel.MEDIUM; return RiskLevel.LOW; } private static double getMetricValue(ImplantMonitoringLog log, String metric) { return switch (metric) { case "neuralLatencyMs" -> log.getNeuralLatencyMs(); case "cpuUsagePct" -> log.getCpuUsagePct(); case "powerUsageUw" -> log.getPowerUsageUw(); default -> 0.0; }; } Finally, we can return an IncidentAssessment object in the original action method. Step 3: Find Affected Implants and Rank Them The next action is to find all affected implants. @Action(description = "Find implants affected by the anomaly and assign anomaly scores") public List findAffectedImplants(IncidentSignal signal) { Map> logs = extractLogs(signal); return logs.entrySet().stream() .map(entry -> toAffectedImplant(entry.getKey(), entry.getValue(), signal)) .sorted(Comparator.comparingDouble(AffectedImplant::anomalyScore).reversed()) .toList(); } The findAffectedImplants() method also calls the extractLogs() method. Then, it converts each map entry into an AffectedImplant. That happens in the toAffectedImplant() method. Here, for each implant, we: Compute an anomaly score; Enrich the record with domain data: lot number, model, civilian ID. Then, we sort all implants by anomaly score descending. private AffectedImplant toAffectedImplant( String serialNumber, List logsPerImplant, IncidentSignal signal) { if (logsPerImplant == null || logsPerImplant.isEmpty()) { // No logs means no evidence; score 0, rest unknown. return new AffectedImplant( serialNumber, null, null, null, 0.0); } double anomalyScore = calculateAnomalyScore(logsPerImplant, signal); Optional civilian = civilianService.findCivilianByImplantSerialNumber(serialNumber); if (civilian.isEmpty()) { throw new RuntimeException("No civilian found for implant serial number " + serialNumber); } Civilian c = civilian.get(); String civilianNationalId = c.getNationalId(); Implant implant = c.getImplants() .stream() .filter(i -> serialNumber.equals(i.getSerialNumber())) .findFirst() .orElseThrow(); String lotNumber = String.valueOf(implant.getLotNumber()); String model = implant.getModel(); return new AffectedImplant( serialNumber, lotNumber, model, civilianNationalId, anomalyScore); } private double calculateAnomalyScore(List logsPerImplant, IncidentSignal signal) { double threshold = signal.threshold(); if (threshold <= 0.0) return 0.0; double max = logsPerImplant.stream() .mapToDouble(l -> getMetricValue(l, signal.metric())) .max() .orElse(0.0); if (max <= threshold) return 0.0; double score = (max - threshold) / threshold; // exceed ratio return Math.min(1.0, score); } So, now we know not only which implants are affected, but also which of them are affected to the worst degree. Step 4: Generate a Root Cause Hypothesis (LLM-Driven Step) The next action, the makeRootCauseHypothesis() method, is where we will use the LLM for reasoning as we will ask it to hypothesize on the anomaly causes based on the provided evidence. @Action(description = "Infer a root cause hypothesis from the evidence") public RootCauseHypothesis makeRootCauseHypothesis(IncidentSignal signal, IncidentAssessment assessment, List affectedImplants, OperationContext context) { return context.ai().withDefaultLlm().createObject( """ Based on the incident details, choose a root cause hypothesis. Rules: - type must be one of: FIRMWARE_REGRESSION, BAD_LOT, ATTACK_PATTERN, ENVIRONMENTAL - confidence is 0..1 - evidence is a short bullet list of specific signals from the inputs IncidentSignal: %s Triage: %s Top affected implants: %s """.formatted(signal, assessment, affectedImplants.stream().limit(10).toList()), RootCauseHypothesis.class ); } The inputs are: The incident signal; The risk assessment; The top 10 affected implants. The LLM must return a typed RootCauseHypothesis with: A type enum: firmware regression, bad lot, attack pattern, or environmental case; Confidence between 0 and 1; A short evidence list. This is intentionally bounded so that the LLM could pick a structured hypothesis. Step 5: Generate a Containment Plan (LLM with Constraints) The action for the planContainment() method creates the response plan. Here, we also use the LLM, but with constraints. @Action(description = "Create a containment plan based on the hypothesis and blast radius") public ContainmentPlan planContainment( IncidentAssessment assessment, RootCauseHypothesis hypothesis, List affectedImplants, OperationContext context) { boolean requiresApproval = assessment.riskLevel() == RiskLevel.HIGH || assessment.riskLevel() == RiskLevel.CRITICAL || hypothesis.type() == HypothesisType.ATTACK_PATTERN; EstimatedBlastRadius radius = estimateRadius(assessment.signal(), affectedImplants); return context.ai().withDefaultLlm().createObject( """ Produce a ContainmentPlan JSON object. Rules: - steps must be a list of objects like: { "text": "..." } - 4-8 steps max, short imperative text Inputs: - riskLevel: %s - hypothesis: %s - blastRadius: %s """.formatted(assessment.riskLevel(), hypothesis, radius), ContainmentPlan.class ); } First, we decide whether the plan needs approval. We require approval if the risk is HIGH or CRITICAL, or the hypothesis looks like an ATTACK_PATTERN. Then, we estimate the blast radius in the estimateRadius() method. private EstimatedBlastRadius estimateRadius(IncidentSignal signal, List affectedImplants) { if (affectedImplants == null) affectedImplants = List.of(); int affectedEstimate = affectedImplants.size(); List affectedLots = affectedImplants.stream() .map(AffectedImplant::lotNumber) .filter(s -> s != null && !s.isBlank()) .distinct() .limit(5) .toList(); List affectedModels = affectedImplants.stream() .map(AffectedImplant::model) .filter(s -> s != null && !s.isBlank()) .distinct() .limit(5) .toList(); String geoSummary = "Within %.0fm of (%.5f, %.5f)" .formatted(signal.radiusMeters(), signal.latitude(), signal.longitude()); String timeSummary = "From %s to %s".formatted(signal.from(), signal.to()); return new EstimatedBlastRadius( affectedEstimate, affectedLots, affectedModels, geoSummary, timeSummary ); } That method produces: How many implants are affected; Affected lots, up to five; Affected models, up to five; A geo summary string; A time summary string. Only after that, we call the LLM and force it into a structured ContainmentPlan with 4 to 8 steps, short imperative instructions, and step objects like { "text": "..." }. This way, we will get a usable plan. Step 6: Assemble the Incident Case (Goal) The last action, the buildIncidentCase() method, is marked with the @AchievesGoal annotation. This is the “done” step that produces the final output object IncidentCase. @AchievesGoal(description = "Investigate an incident signal and produce a complete incident case", examples = { "Investigate telemetry anomalies near (lon,lat) in a time window and propose containment", "Assess implant incident risk and recommend actions" }) @Action(description = "Assemble a complete IncidentCase") public IncidentCase buildIncidentCase( IncidentSignal signal, IncidentAssessment assessment, List affected, RootCauseHypothesis hypothesis, ContainmentPlan plan) { return new IncidentCase( UUID.randomUUID().toString(), Instant.now(), signal, assessment, affected, hypothesis, plan ); } The incident case bundles everything the agent gathered so far: ID; Timestamp; The original signal; Incident assessment; Affected implants; Hypothesis; Containment plan. So, one user message becomes a complete incident ticket. Run the application and paste something like that into the Embabel console: x "Center: lat 40.7580 lon -73.9855, radius 1200m, yesterday 13:00–23:00, metric neuralLatencyMs, threshold 120" You will see in the logs how the agent is planning to reach the goal, and as an output, you will get the complete incident case in a JSON format. Congratulations, we have built an agentic workflow on the JVM, where the LLM is used for the squishy parts such as parsing, hypothesis, and planning, and Java handles the hard truth of querying, scoring, ranking, and rules. And that’s the whole point: you get an agent that’s smart, but not uncontrolled. Conclusion Embabel is a great fit when you want to spice up your application with agentic behavior without giving up JVM reliability. By modeling the workflow as typed goals and actions, we use the LLM only where it helps, for instance, for parsing and bounded reasoning, and keep the rest deterministic. If you want more content like this on agentic workflows and modern Java development, subscribe to our blog. - [An Overview of JDK 26 Features](https://bell-sw.com/blog/an-overview-of-jdk-26-features/): JDK 26 is a non-LTS release, but it doesn’t make it less important! It is always interesting to see where Java is heading, and for the teams planning to upgrade to the next LTS version — to try out new features in advance. This release includes 10 JEPs with new, improved, or removed functionality. Let’s look at all of them. Table of Contents New Features JEP 500: Prepare to Make Final Mean Final JEP 517: HTTP/3 for the HTTP Client API JEP 522: G1 GC: Improve Throughput by Reducing Synchronization Improved Features JEP 516: Ahead-of-Time Object Caching with Any GC JEP 524: PEM Encodings of Cryptographic Objects (Second Preview) JEP 525: Structured Concurrency (Sixth Preview) JEP 526: Lazy Constants (Second Preview) JEP 529: Vector API (Eleventh Incubator) JEP 530: Primitive Types in Patterns, instanceof, and switch (Fourth Preview) Removed Features JEP 504: Remove the Applet API Conclusion New Features JEP 500: Prepare to Make Final Mean Final JEP 500 aims to prepare the developers for the restriction of mutation of final fields in future releases. Final fields in Java represent an immutable state. After they are assigned in a constructor or in a class initializer, a final field cannot be reassigned. The expectation that a final field cannot be reassigned is important for reliability and even app performance. However, there are several APIs in Java that allow final fields to be reassigned at any time by any code. That undermines the whole point of final fields. // A normal class with a final field class C { final int x; C() { x = 100; } } // 1. Perform deep reflection over the final field in C java.lang.reflect.Field f = C.class.getDeclaredField("x"); f.setAccessible(true); // Make C's final field mutable // 2. Create an instance of C C obj = new C(); System.out.println(obj.x); // Prints 100 // 3. Mutate the final field in the object f.set(obj, 200); System.out.println(obj.x); // Prints 200 f.set(obj, 300); System.out.println(obj.x); // Prints 300 With JEP 500, developers will receive warnings about uses of deep reflection to mutate final fields. To avoid current warnings and future restrictions, developers will have to explicitly and selectively enable the ability to mutate them. JEP 517: HTTP/3 for the HTTP Client API JEP 517 updates Java's HTTP Client API to support the HTTP/3 protocol, which was standardized in 2022 and is supported by most web browsers. Java applications using HTTP/3 can benefit from a more reliable transport and potentially faster handshakes. HTTP/2 remains the default version of the HTTP Client API, so developers can use the new version by setting the protocol version of an HttpClient object to HTTP/3. If the target server does not support HTTP/3 then the request will be transparently downgraded to HTTP/2 or HTTP/1.1. import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class Http3Demo { public static void main(String[] args) throws Exception { var client = HttpClient.newHttpClient(); var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/")) .version(HttpClient.Version.HTTP_3) .GET() .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.version()); // HTTP_3, HTTP_2, or HTTP_1_1 System.out.println(response.statusCode()); } } JEP 522: G1 GC: Improve Throughput by Reducing Synchronization JEP 522 aims to improve the throughput of applications that use G1 GC by reducing the G1 synchronization overhead. G1GC keeps track of object references so it can update them efficiently after moving objects. But some apps update references so often that G1 needs background optimizer threads, and those threads have to synchronize with application threads to avoid being on each other’s way. That coordination makes G1’s write barriers slow. The JEP addresses this problem by letting the application threads and the GC optimizer threads work on separate copies of the tracking data and swapping them when needed. This will boost throughput without changing how users interact with G1. Improved Features JEP 516: Ahead-of-Time Object Caching with Any GC JEP 516 further improves the AOT Cache feature introduced as part of the Project Leyden integration in JDK 24. AOT Cache enables you to reduce the startup and warmup time of Java applications by creating a cache with loaded and linked classes and method profiles. Previously, this cache was incompatible with ZGC, a low-latency Java garbage collector. But now, it can be used with all garbage collectors, including ZGC, so you don’t have to choose between low latency and fast startup. // trial run of the application java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App // production run of the application java -XX:AOTCache=app.aot -cp app.jar com.example.App JEP 524: PEM Encodings of Cryptographic Objects (Second Preview) PEM Encodings of Cryptographic Objects is an API for encoding objects representing cryptographic keys, certificates, and certificate revocation lists into the Privacy-Enhanced Mail (PEM) transport format, and for decoding from that format back into objects. JEP 524 re-previews this feature with several changes, including some renamed classes and methods. JEP 525: Structured Concurrency (Sixth Preview) Structured concurrency was first introduced in JDK 19. Its goal is to improve the experience of working with concurrency in Java and help the developers write more readable, maintainable, and reliable code. The secret ingredient of the feature is tying the lifetime of subtasks to a specific scope with clearly defined entry and exit points, namely the task’s code block. This way, errors, cancellation, and observability all follow a clear, enforced parent-child structure. If one subtask fails, for instance, others are automatically cancelled. import java.util.concurrent.StructuredTaskScope; import java.util.concurrent.StructuredTaskScope.Subtask; record Response(String user, int order) {} class Service { Response handle() throws InterruptedException { try (var scope = StructuredTaskScope.open()) { Subtask user = scope.fork(this::findUser); Subtask order = scope.fork(this::fetchOrder); scope.join(); // Join subtasks, propagating exceptions // Both subtasks have succeeded, so compose their results return new Response(user.get(), order.get()); } } } JEP 525 re-previews the feature with minor changes to gain additional feedback. JEP 526: Lazy Constants (Second Preview) Lazy constants are objects holding immutable data. Like final fields, they are treated as constants by the JVM and allow for the same optimizations. But unlike finals, they don’t have to be initialized at the moment of creation. This allows the application to initialize them incrementally and start faster. Lazy constants were first introduced in JDK 25 as Stable Values. JEP 526 gives them a new name and shifts their purpose towards high-level use cases, removing low-level methods. In addition, lazy collection helpers are now exposed as List.ofLazy and Map.ofLazy. Also, some extra factories were removed, and computed null values are no longer allowed. import java.lang.LazyConstant; import java.util.List; public class LazyConstantsDemo { // A lazy constant stored in a final field for best JVM optimizations private static final LazyConstant LOGGER = LazyConstant.of(() -> Logger.create(LazyConstantsDemo.class)); // Lazy list: each element is initialized on first access private static final List workers = List.ofLazy(3, i -> new Worker("worker-" + i)); public static void main(String[] args) { System.out.println("App started (without eagerly creating everything)."); // First access triggers initialization for element 0 workers.get(0).doWork(); // Second access reuses the same already-initialized element workers.get(0).doWork(); // Logger is also initialized on first use LOGGER.get().info("Lazy constants are doing their job."); } static final class Worker { private final String name; Worker(String name) { this.name = name; } void doWork() { System.out.println(name + " working"); } } } JEP 529: Vector API (Eleventh Incubator) JEP 529 re-incubates the Vector API without significant changes. The Vector API was first introduced in JDK 16 for expressing vector computations that reliably compile at runtime to optimal vector instructions. This API will incubate until necessary features of Project Valhalla become available as preview features. After that, the Vector API will be promoted from incubation to preview. JEP 530: Primitive Types in Patterns, instanceof, and switch (Fourth Preview) JEP 530 brings two important changes to the feature that allows using primitive types in Patterns, instanceof, and switch. Firstly, the JEP tightens the rules the compiler uses to decide when a conversion is guaranteed to not lose information. The feature now clearly covers both always-safe type conversions (like byte to int, int to double) and constants that are safe to convert (like the literal 42 to byte). The second change brings tighter dominance checks to primitive types in switch. It means that switch now rejects case labels that can never be reached because an earlier case already matches everything that the following case could match. int j = 16_777_216; String s = switch (j) { case float f -> "float: " + f; case 16_777_216 -> "never reached now"; // error: dominated since 16_777_216 can be // converted unconditionally exactly to float default -> "other"; }; Removed Features JEP 504: Remove the Applet API JEP 504 removes the Applet API: the whole java.applet package, classes related to applets, and also any remaining elements that reference this API. Modern web browsers don’t support applets anymore, so there is no reason to keep this API in the Java platform. Conclusion Early-access builds of JDK 26 are available so you can experiment with them now or wait for the GA of your favorite distribution due on March 17th. - [Liberica JDK 26 is Released](https://bell-sw.com/blog/liberica-jdk-26-is-released/): We are happy to announce the general availability of Liberica JDK 26 builds! The new version contains: 2,825 fixes overall — 2,665 in JDK and 160 in FX. BellSoft contributed 9 fixes to this release. 10 JEPs with new, improved, or removed features. Download Liberica JDK 26 Summary of JEPs in JDK 26 New Features JEP 500: Prepare to Make Final Mean Final makes the JVM issue warnings about uses of deep reflection to mutate final fields. JEP 517: HTTP/3 for the HTTP Client API updates the Java’s HTTP Client API to support the HTTP/3 protocol. JEP 522: G1 GC: Improve Throughput by Reducing Synchronization reduces the amount of synchronization required between application threads and G1 GC threads. Enhanced Features JEP 516: Ahead-of-Time Object Caching with Any GC allows all garbage collectors, including Z GC, to work smoothly with the AOT cache introduced by Project Leyden. JEP 524: PEM Encodings of Cryptographic Objects (Second Preview) re-previews the API for encoding/decoding cryptography-related objects with several changes, including new methods and better alignment of exceptions. JEP 525: Structured Concurrency (Sixth Preview) re-preview the feature with minor changes only to get additional feedback. JEP 526: Lazy Constants (Second Preview) revises the feature and introduces a second preview with several important changes, including the new name and the re-orientation of the feature to higher-level use cases. JEP 529: Vector API (Eleventh Incubator) re-incubates the Vector API without significant changes. JEP 530: Primitive Types in Patterns, instanceof, and switch (Fourth Preview) re-previews the feature with improved definition of unconditional exactness and tighter dominance checks in switch constructs. Removed Features JEP 504: Remove the Applet API removes the entire java.applet package and related classes. You can read more about each JEP in our Overview of JDK 26 Features or watch a dedicated video overview. Download Liberica JDK 26 builds now! Regardless of whether you are planning to migrate to the next LTS release or just want to play around with new Java features, you can download and use Liberica JDK for free. Head over to the Liberica JDK Download Center to get the fresh JDK builds for your platform. Download Liberica JDK 26 - [Liberica JDK 8u492, 11.0.31, 17.0.19, 21.0.11, 25.0.3, and 26.01 builds are available](https://bell-sw.com/blog/liberica-jdk-8u492-11-0-31-17-0-19-21-0-11-25-0-3-and-26-01-builds-are-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u501, 7u501, 8u491, 11.0.30.0.1, 17.0.18.0.1, 21.0.10.0.1, 25.0.2.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u492, 11.0.31, 17.0.19, 21.0.11, and 25.0.3, 26.01 with non-critical fixes and general improvements. The release contains 954 fixes and backports overall. BellSoft participated in eliminating 63 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 10 security issues (CVEs) fixed. 78 total security fixes (+ 51 additional non-security fixes) in CPU release: in Liberica 6u501: 8 security fixes + 8 additional fixes; in Liberica 7u501: 10 security fixes + 13 additional fixes; in Liberica 8u491: 11 security fixes + 8 additional fix; in Liberica 11.0.30.0.1: 12 security fixes + 7 additional fixes; in Liberica 17.0.18.0.1: 12 security fixes + 6 additional fixes; in Liberica 21.0.10.0.1: 12 security fixes + 4 additional fixes; in Liberica 25.0.2.0.1: 13 security fixes + 5 additional fixes. In addition, PSU releases include a total of 825 fixes and backports: in Liberica 8u492: 11 security fixes (+ 1 in FX) + 54 additional fixes (+ 10 in FX); in Liberica 11.0.31: 12 security fixes (+ 1 in FX) + 47 additional fixes (+ 10 in FX); in Liberica 17.0.19: 12 security fixes (+ 1 in FX) + 99 additional fixes (+ 14 in FX); in Liberica 21.0.11: 12 security fixes (+ 1 in FX) + 147 additional fixes (+ 13 in FX). in Liberica 25.0.3: 13 security fixes (+ 1 in FX) + 308 additional fixes (+ 7 in FX); in Liberica 26.0.1: 13 security fixes (+ 1 in FX) + 20 additional fixes (+ 17 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2026-22016 7.5 xml jaxp network low none none unchanged high none none CVE-2026-34282 7.5 core-libs java.net network low none none unchanged none none high CVE-2026-22021 5.3 security-libs java.security network low none none unchanged none none low CVE-2026-22013 5.3 security-libs org.ietf.jgss network high none required unchanged high none none CVE-2026-23865 5.3 client-libs 2d local low none required unchanged low low low CVE-2026-22008 3.7 core-libs java.lang network high none none unchanged none low none CVE-2026-22018 3.7 core-libs java.util network high none none unchanged none none low CVE-2026-22007 2.9 security-libs java.security local high none none unchanged low none none CVE-2026-34268 2.9 security-libs java.security local high none none unchanged low none none CVE-2026-20652 7.5 javafx web network low none none unchanged none none high Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 25 26 CVE-2026-22016 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-34282 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-22021 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-22013 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-23865 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-22008 𑇐 𑇐 CVE-2026-22018 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-22007 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-34268 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-20652 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [How to Profile Java Applications with JFR: Beginner's Guide](https://bell-sw.com/blog/how-to-profile-java-applications-with-jfr-beginner-s-guide/): Your app is slow, and you've been trying to solve the mystery for an hour. The logs show nothing useful. The GC metrics look vaguely suspicious but not alarming enough to actually blame. JFR doesn't care about your logs. It goes to the JVM itself: CPU usage, thread states, lock contention, GC behavior, allocations, I/O. All of that data is written to a binary file you can open and read. It's been part of OpenJDK since Java 11, and it is also part of Liberica JDK 8 if you are in Java 8. So, you don't need to install anything. Let's look at what JFR can do, how to use it locally and in containers, and how to analyze its reports in JDK Mission Control. Table of Contents What JFR Is (and Isn't) Using JFR Locally JFR in Containers Starting a recording at container startup Using buildpacks Attaching to a running container Reading the Evidence in Mission Control Where to Go Next What JFR Is (and Isn't) Think of JFR as a flight recorder for your JVM. It captures runtime events with timestamps and writes them to a .jfr binary file. You analyze the results in JDK Mission Control, an open-source GUI that organizes events by category and flags suspicious patterns automatically. The overhead is low enough for production use. You can start recording at JVM startup or attach to a running process on demand via jcmd, which is also in your JDK. JFR is the right tool when the question is: What is this JVM actually doing? Where is it spending time? Are threads blocked, are allocations unusually high, is I/O holding things up? It doesn't profile native libraries, it doesn't reconstruct a request's path across multiple services, and it won't tell you why a specific SQL query is slow — for those you need system profilers, distributed tracing, or database tools. But for JVM-level visibility, there's nothing easier to reach for. Using JFR Locally Let's start locally. The simplest way is to start a recording at JVM startup: java -XX:+UnlockDiagnosticVMOptions \ -XX:+DebugNonSafepoints \ -XX:StartFlightRecording=duration=30s,filename=my-recording.jfr -jar app.jar -XX:+UnlockDiagnosticVMOptions and -XX:+DebugNonSafepoints are optional but improve accuracy — they allow the JVM to record method samples at more precise points instead of only at safepoints. If the app is already running, jcmd can start and stop a recording without a restart: jcmd PID JFR.start jcmd PID JFR.dump filename=my-recording.jfr jcmd PID JFR.stop For long-running apps, continuous recording keeps a rolling in-memory buffer. In JDK 25 the default max size is 250 MiB, which you can override: -XX:StartFlightRecording=name=background,maxsize=1g Combine maxsize with maxage=1h to keep the last hour of data, or use dumponexit=true to flush the buffer to disk when the JVM exits. JFR can generate a lot of data, so putting a cap on it keeps overhead manageable. JFR in Containers Profiling a containerized app is a bit more involved, depending on what you have access to. Starting a recording at container startup If you control the image build, you can bake in the JFR flags directly. Here's a multi-stage Dockerfile using BellSoft Hardened Images — minimal, security-locked images built on Liberica JDR recommended by Spring and lightweight Alpaquita Linux: FROM bellsoft/hardened-liberica-runtime-container:jdk-25-stream-musl as builder WORKDIR /app ADD my-app /app/my-app RUN cd my-app && ./mvnw package FROM bellsoft/hardened-liberica-runtime-container:jre-25-stream-musl WORKDIR /app COPY --from=builder /app/my-app/target/*.jar /app/my-app.jar EXPOSE 8081 ENTRYPOINT ["java", \ "-XX:+UnlockDiagnosticVMOptions", \ "-XX:+DebugNonSafepoints", \ "-XX:StartFlightRecording=duration=30s,filename=/tmp/recording.jfr", \ "-XX:MaxRAMPercentage=80.0", \ "-jar", "/app/my-app.jar"] Build and run as usual. The recording starts with the JVM. Using buildpacks If you're building container images with Cloud Native Buildpacks, one environment variable is enough. You can use it with the pack CLI or specify in Maven or Gradle plugin. Here's the Maven example: true By default, this writes the recording to /tmp/recording.jfr on JVM exit. To control the duration and filename, add BPE_APPEND_JAVA_TOOL_OPTIONS: true -XX:StartFlightRecording=duration=15s,filename=/tmp/recording.jfr For either approach, you retrieve the recording from the container with docker cp: docker cp :/tmp/recording.jfr . Attaching to a running container Both approaches above require the JFR flags to be there at startup. If the app is already running with no JFR flags (or if the image is JRE-only, distroless, or has no shell), you can't exec in and run jcmd. Ephemeral containers solve this. They're temporary containers that share the process namespace of another running container, which means jcmd inside the ephemeral container can see the JVM in the app container. The ephemeral image only needs a JDK: FROM bellsoft/hardened-liberica-runtime-container:jdk-25-stream-musl Run the app container normally. Then attach the ephemeral container to it: docker run --cap-add SYS_PTRACE \ --security-opt=apparmor:unconfined \ --name ephemeral \ --pid=container:app \ -it prof-jcmd sh --pid=container:app puts the ephemeral container in the same PID namespace. SYS_PTRACE lets it read JVM performance data. Inside the ephemeral container, list the running JVMs and start a recording: When you are inside the ephemeral container, you use jcmd commands to start and dump a JFR recording: jcmd JFR.start jcmd JFR.dump filename=/tmp/recording.jfr jcmd JFR.stop When done, copy the recording from the app container (not the ephemeral one): docker cp :/tmp/recording.jfr . Reading the Evidence in Mission Control Let's look at what a real recording actually tells you. I took a 60-second JFR recording of Neurowatch, a small Spring Boot demo service, and opened it in Liberica Mission Control, a distribution of JDK Mission Control. Automated Analysis Results tab in Liberica Mission Control The first thing Mission Control shows you is the Automated Analysis Results page: rule-based findings and tuning hints. Looking at Neurowatch's recording, there's a lot of memory used, long socket read pauses, dozens of exceptions, and GC behavior possibly indicating a memory leak. Wow. So much for a small demo service. The rule-based findings flag what looks suspicious and suggest where to dig first. Take them as a signal, then go look. The recording was taken during app startup, and that context changes how you interpret everything. Garbage Collection tab GC is active, but the pauses are short. A few milliseconds each. The JVM is collecting often enough to show it's allocating, but handling it comfortably. GC is not the problem here. Memory tab byte[] accounts for around 60% of all allocations, followed by a Spring Boot class loader, URL, String, and int. That's buffer churn, string processing, and framework initialization: exactly what startup looks like. Threads tab The Threads tab shows whether the JVM is actively working or mostly waiting. Zooming into the timeline gives you a detailed breakdown of any thread's state at any moment during the recording window. Methods tab From the method profiling view, you can see how the application spent its time down to the method name. BCrypt.encipher appears near the top, which means some password hashing or security setup happened during the recording window. BCrypt is expensive by design, so this is expected if login or security initialization runs at startup. Exceptions tab The raw exception count is high. Most trace back to classloading, reflection, and framework internals — the usual noise of startup. Keep an eye on it if you profile during live traffic. Environment tab At the Environment tab: JVM configuration and hardware details from the moment of capture. Especially useful when you're reading someone else's recording. The full picture: Neurowatch wasn't in trouble. We captured a Spring Boot app starting up — framework initialization, classpath scanning, security configuration, preload work. That explains the allocations, the short GC pauses, the BCrypt work, and the exception count. Where to Go Next Download JDK Mission Control and take a 30-second recording of any Java app you're running. The Automated Analysis page gives you something to look at immediately — even if it turns out the JVM was perfectly fine all along. When you are ready to go further, BellSoft has a few articles that discuss more advanced topics on Java profiling with JFR: Java Flight Recorder: a gem hidden in OpenJDK JDK Flight Recorder: The Programmatic Way Hunting down code hotspots with JDK Flight Recorder Hunting down memory issues with JDK Flight Recorder - [The builds of Liberica NIK 23.0.12, 23.1.11, and 25.0.3 are generally available](https://bell-sw.com/blog/the-builds-of-liberica-nik-23-0-12-23-1-11-and-25-0-3-are-generally-available/): Liberica Native Image Kit 23.0.12, 23.1.11, and 25.0.3 builds are released We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.12 for JDK 17, 23.1.11 for JDK 21, and 25.0.3 for JDK 25 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. Liberica NIK releases are aligned with GraalVM release schedule. Starting with JDK 20 release in March 2023, GraalVM CE conforms to the six-month JDK release cadence. CPU builds become available four times a year as before. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Download Liberica NIK Notable improvements List of security issues fixed The following vulnerabilities are fixed in all three releases — 23.0.12 (JDK 17), 23.1.11 (JDK 21), and 25.0.3 (JDK 25): 23.0.12 (JDK 17) and 23.1.11 (JDK 21) CVE ID CVSS score Component Module Attack vector CVE-2026-22016 7.5 xml jaxp network CVE-2026-34282 7.5 core-libs java.net network CVE-2026-22021 5.3 security-libs java.security network CVE-2026-22013 5.3 security-libs org.ietf.jgss network CVE-2026-23865 5.3 client-libs 2d local CVE-2026-22018 3.7 core-libs java.util network CVE-2026-22007 2.9 security-libs java.security local CVE-2026-34268 2.9 security-libs java.security local The 25.0.3 (JDK 25) build additionally addresses the following vulnerability: CVE ID CVSS score Component Module Attack vector CVE-2026-22008 3.7 core-libs java.lang network Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK - [Spring Developers Have a Blindspot When It Comes to Container Security](https://bell-sw.com/news/spring-developers-have-a-blindspot-when-it-comes-to-container-security/): Spring Developers Have a Blindspot When It Comes to Container Security A survey from BellSoft found that Spring developers don’t know their Dockerfiles affect their security posture, aren’t using hardened images and can’t name their compliance framework, exposing their organizations, applications and users to considerable risk San Jose, California (May 21, 2026) BellSoft announces the publication of a new report, “Security in the Blind Spot: What Spring Developers Don't Know About Their Own Containers,” including the results of a survey of developers conducted last month at Spring I/O in Barcelona. BellSoft surveyed 250 Spring developers, DevOps engineers and Java architects on-site at Spring I/O 2026, one of the most significant annual events in the European Java ecosystem. The survey probed not just tool adoption but the underlying knowledge gaps, decision-making structures and practices that determine whether Java container deployments are secure. Here are the key findings: 64% of Spring developers didn’t know their Dockerfile was a security risk The most significant finding in this survey was not a gap in tooling but knowledge. Sixty-four percent of respondents at Spring I/O, among the most engaged practitioners in the European Java ecosystem, had never considered that Dockerfile authoring decisions directly affected their security posture. 42% of survey respondents had never heard of hardened images Only 22% of respondents currently use hardened container images in production, and 42% have never encountered the concept at all. This is a structural awareness gap: adoption cannot outpace knowledge. The 14% who said they are interested but haven’t started yet, and the seven percent who are planning adoption, represent a pipeline, but one that requires education before it converts to practice. 44% of engineers couldn’t name the compliance rules governing their container stack DORA and ISO 27001 each applied to 22% of surveyed organizations, with NIS2 adding an additional 12%. These are not aspirational frameworks. They are in force today, with binding requirements for software supply chain security, vulnerability management and digital resilience. Their engineering implications are direct: image provenance, CVE patching cadence, SBOM generation and incident response all fall within scope. And yet, 44% of respondents answered “not sure, managed by another team,” when asked about their compliance framework. This is not necessarily negligence: large organizations route compliance through dedicated GRC functions, and developers are often shielded from the specifics. But when engineers don’t know which frameworks apply, they cannot build systems that meet them. The connection between daily engineering decisions (base image selection, patching cadence, signing, etc.) and regulatory obligations must be better understood at the practitioner level. 16% of respondents apply zero of the five most important container security practices These five practices -- scanning, hardening, patching, SBOMs and image signing -- form a layered container security defense. Each layer compensates for the gaps in the others. Fewer than 2% of respondents have all five in place, approximately 65% apply zero or one practice, and 16% apply none at all, relying on cloud providers to manage a security domain that cloud providers explicitly do not own under the shared responsibility model. “Container security is no longer a niche concern for platform engineers,” said Alex Belokrylov, CEO at BellSoft. “Developers are woefully under-informed about the scope of this issue, and the data is clear: controls embedded at the platform level achieve universal, consistent coverage, whereas controls that depend on individual developer awareness do not. The urgent priority is education, the second is automation.” The complete BellSoft 2026 Spring I/O report can be found here. - [JDBC vs ORM vs jOOQ: Choosing the Right Java Database Stack](https://bell-sw.com/blog/jdbc-vs-orm-vs-jooq-choosing-the-right-java-database-stack/): JDBC, JPA, Hibernate, jOOQ. Are these five different ways to connect to a database? Not exactly. They're not competing tools. They're layers, and the funny thing is that most of the time, you're using several of them without tracking which one is doing what. As I was writing this article, a metaphor, which makes the stack clearer, came to my mind. Communicating with a database is like communicating in a foreign language. If you speak the language yourself, you write every sentence directly. That's JDBC. If you hire a translator, you speak your native language and let them handle the SQL. That's ORM. JPA, Hibernate, and Blaze Persistence live in this category. If you hire a corrector instead, someone who lets you write in the foreign language but checks your work for mistakes before it goes out, that's jOOQ. The choice between them isn't about preference. It's about how much control you need, and what you're willing to trade for it. In this article, we will look at all these concepts, their pros and cons, and when to use which. Table of Contents JDBC: Write It Yourself ORM and JPA: Hire a Translator Hibernate: A Very Good Translator (Mostly) Where Hibernate Struggles jOOQ: Hire a Corrector Instead Pick Your Remedy JDBC: Write It Yourself JDBC or Java Database Connectivity has been part of Java since version 1.1. It's the low-level API for communicating with relational databases, and everything else in this article eventually calls it. With JDBC, you write SQL yourself, manage connections yourself, and map result rows to objects yourself: public class BaseRepository { protected final JdbcTemplate jdbc; protected final RowMapper mapper; public BaseRepository(JdbcTemplate jdbc, RowMapper mapper) { this.jdbc = jdbc; this.mapper = mapper; } protected Optional findOne(String query, Object... params) { T result = jdbc.queryForObject(query, mapper, params); return Optional.ofNullable(result); } protected List findMany(String query, Object... params) { return jdbc.query(query, mapper, params); } protected boolean delete(String query, long id) { int rowsDeleted = jdbc.update(query, id); return rowsDeleted > 0; } protected void update(String query, Object... params) { int rowsUpdated = jdbc.update(query, params); if (rowsUpdated == 0) { throw new InternalServerException("Couldn't update data"); } } protected long insert(String query, Object... params) { GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); jdbc.update(connection -> { PreparedStatement ps = connection .prepareStatement(query, Statement.RETURN_GENERATED_KEYS); for (int idx = 0; idx < params.length; idx++) { ps.setObject(idx + 1, params[idx]); } return ps;}, keyHolder); Long id = keyHolder.getKeyAs(Long.class); if (id != null) { return id; } else { throw new InternalServerException("Couldn't save data"); } } } As you can see from the snippet above, relying on JDBC only will make the code quite verbose. Every query is a string, and every mapping is manual. But the payoff is complete transparency. There’s no generated SQL to hunt in logs and no framework behavior to work around. Essentially, what you see is what you get, or rather, what you wrote is what you run. JDBC (or Spring Data JDBC, which adds a thin convenience layer on top) is the right call for a small, focused service with a stable set of queries and minimal dependencies. However, it gets expensive quickly once you have dozens of entities and relationships. ORM and JPA: Hire a Translator Sometimes you don't want to write SQL. You have a domain model, you have Java objects, and you'd rather manipulate those objects and let something else figure out the database part. That's ORM, Object-Relational Mapping. The framework translates your object operations into SQL. Most of the time, you don't even see the queries. JPA, Java Persistence API, is the specification for how that translation should work. It defines the annotations, the interfaces, and the contract between your entities and the ORM tool: @PersistenceContext private EntityManager em; @Transactional public void save(Product product) { em.persist(product); } @Entity @Table(name = "products") public class Product { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn(name="supplier_id") private Supplier supplier; } Note that JPA doesn't do any actual work: it's a set of interfaces and rules. To use it, you need an implementation. The one almost everyone uses is Hibernate. Hibernate: A Very Good Translator (Mostly) Hibernate is the most widely used JPA implementation. When developers say "I'm using JPA," they mean Hibernate roughly 90% of the time. On top of the standard JPA APIs, Hibernate adds its own native API (Session, SessionFactory), extra mapping annotations, SQL dialect support for PostgreSQL, Oracle, MySQL, and many others, plus some cherry-on-the-pie features. For example, caching is one of these features. The first-level cache stores entities in use within a specific session. This cache is on by default. The second-level cache keeps frequently accessed data in memory across sessions, so the database gets fewer hits for anything read repeatedly. Enable it with @Cacheable and a CacheConcurrencyStrategy: @Entity @Table(name = "products") @Cacheable @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Product { } With Hibernate, standard one-to-many and many-to-many relationships are a solved problem. You annotate your entities, Hibernate writes the joins. It plugs into Spring, Quarkus, and Micronaut cleanly. Auditing, validation, and event hooks are built in or attached without much ceremony. For a domain-driven application with a conventional relational model and standard CRUD, Hibernate earns its place. But don’t get excited over Hibernate too early. Gavin King, Hibernate's creator, put the nuance plainly: "Just because you are using Hibernate, doesn't mean you have to use it for everything." So, let’s look at cases where Hibernate struggles. Where Hibernate Struggles The N+1 problem is Hibernate's most famous trap. You fetch a list of parent records, then access a related collection on each one: public void printSuppliersWithProducts() { // First query: get all suppliers List suppliers = em.createQuery( "select s from Supplier s", Supplier.class ).getResultList(); // For each supplier, accessing products triggers another SELECT for (Supplier supplier : suppliers) { // This triggers a separate query per supplier List products = supplier.getProducts(); System.out.println("Supplier: " + supplier.getName()); for (Product product : products) { System.out.println(" Product: " + product.getName()); } } } 50 suppliers. 51 queries. The application feels fine in development with 5 rows and surfaces the problem in production with 5,000. This isn't a Hibernate bug; rather, it's the consequence of the ORM trade-off. The translator doesn't know you were going to ask about the children until you ask. Beyond N+1, Hibernate has some other ceilings. Window functions like RANK() and running totals require falling back to native SQL queries. Anti-joins like "give me all rows with no match in this other set" are technically possible but awkward. JSON column mapping doesn't fit the entity model. Partial composite key references trip Hibernate up. Then, there's equals/hashCode. Implementing these correctly for JPA entities is a discipline of its own. Get them wrong and you get subtle bugs in collections, session state, and second-level cache behavior. In addition, a toString() can trigger lazy loading on every field. jOOQ: Hire a Corrector Instead jOOQ or Java Object Oriented Querying takes the opposite bet from Hibernate. Instead of abstracting SQL away, it gives you a way to write SQL in Java with full type safety. The model is database-first. jOOQ generates Java classes from your existing schema: tables, columns, enums, and foreign keys, all as typed Java objects. You reference PRODUCT.ID, not the string "product.id". If you rename a column in the schema and regenerate, any query referencing the old name stops compiling. In short, the database schema is the source of truth. So, the errors surface at build time. Under the hood, jOOQ translates the DSL back to SQL and executes it via JDBC. The queries where jOOQ's approach pays off most clearly are the ones Hibernate turns into a series of problems. One good example of such a query is fetching a parent record with multiple nested child collections. This is Hibernate's N+1 problem one query per parent record, for however many parents you have. jOOQ's MULTISET takes a different approach: Field> bookings = multiset( select( BOOKING.ID, BOOKING.STATUS, BOOKING.CREATED_AT, FACILITY.NAME, APPOINTMENT_SLOT.STARTS_AT, STAFF.HANDLE ) .from(BOOKING) .join(APPOINTMENT_SLOT).on(APPOINTMENT_SLOT.ID.eq(BOOKING.APPOINTMENT_SLOT_ID)) .join(FACILITY).on(FACILITY.ID.eq(APPOINTMENT_SLOT.FACILITY_ID)) .leftJoin(STAFF).on(STAFF.ID.eq(BOOKING.STAFF_ID)) .where(BOOKING.TRIAGE_CASE_ID.eq(TRIAGE_CASE.ID)) .orderBy(APPOINTMENT_SLOT.STARTS_AT.asc(), BOOKING.CREATED_AT.asc()) ).as("bookings") .convertFrom(rs -> rs.map(r -> new BookingDto( r.get(BOOKING.ID), r.get(BOOKING.STATUS).toString(), r.get(BOOKING.CREATED_AT), r.get(FACILITY.NAME), r.get(APPOINTMENT_SLOT.STARTS_AT), r.get(STAFF.HANDLE) ))); MULTISET collects the results of a correlated subquery into a typed nested collection, a List on the parent record, in a single database round-trip. There's no session state to manage, no lazy loading proxy watching your field accesses, and no queries running behind the scenes that you didn't write. The database does what databases are good at. jOOQ maps the structured result back to typed Java collections. This pattern extends to multiple levels of nesting (children of children) without the query count growing. In addition to that, jOOQ handles window functions, CTEs, conditional expressions, and most of what Hibernate sends to native SQL territory. All of that is type-safe and in the same fluent DSL. Pick Your Remedy As usual, no single tool wins across all use cases, so the decision is yours to make based on your project. For a small service with a handful of stable queries, JDBC is the most honest choice. Hibernate earns its keep in domain-driven applications where the relations are conventional and most operations are standard CRUD. You get mapping, caching, and framework integration without writing SQL for the common cases. When SQL is central to the application (reporting queries, window functions, nested payloads, database-specific features) jOOQ is the right choice. Type-safe SQL with compile-time feedback is a real advantage when query complexity reaches the point where assembling strings becomes a liability. The good news is that you can take the best of two worlds and use Hibernate and jOOQ in the same project. Hibernate will handle the CRUD and complex writes, jOOQ will handle complex reads. - [Overview of Java 27 Features](https://bell-sw.com/blog/overview-of-java-27-features/): The next JDK 27 release due in September includes 9 JEPs with new and enhanced features. Although this is a non-LTS release, JDK 27 is still interesting to check out, especially for team planning the upgrade to the next LTS version. So, let’s look at what JDK 27 has in stock! Table of Contents New Features JEP 523: Make G1 the Default Garbage Collector in All Environments JEP 527: Post-Quantum Hybrid Key Exchange for TLS 1.3 JEP 534: Compact Object Headers by Default JEP 536: JFR In-Process Data Redaction Improved Features JEP 531: Lazy Constants (Third Preview) JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview) JEP 533: Structured Concurrency (Seventh Preview) JEP 537: Vector API (Twelfth Incubator) JEP 538: PEM Encodings of Cryptographic Objects (Third Preview) Conclusion New Features JEP 523: Make G1 the Default Garbage Collector in All Environments OpenJDK offers not one but seven garbage collectors. Each of them is tailored to a specific scenario, so developers can choose a collector based on the performance requirements for throughput, latency, footprint, and startup time. But before Java 27, there was a catch. If the app was allocated a single CPU or less than 1792 MB of physical memory, Serial GC was chosen automatically. This was done intentionally, because years ago, tests showed that Serial GC had advantages in throughput and footprint in such environments. A lot has changed since then. G1 GC, the default garbage collector in the JVM, has been constantly improved. Its latency has always been better than that of Serial GC. And now, G1 GC can also compete with Serial GC at all heap sizes. Therefore, JEP 523 makes G1 GC the default collector in all environments regardless of the number of processors and the available physical memory. JEP 527: Post-Quantum Hybrid Key Exchange for TLS 1.3 JEP 527 makes Java’s TLS 1.3 connections more ready for the post-quantum world by adding hybrid key exchange algorithms. They combine today’s proven cryptography with newer quantum-resistant cryptography. The idea is to protect data from “harvest now, decrypt later” attacks, where someone stores encrypted traffic today and tries to break it later with a quantum computer. For most Java apps using javax.net.ssl, this should work by default, with no code changes unless the code already selects specific key exchange schemes. JEP 534: Compact Object Headers by Default Compact Object Headers have recently been integrated into OpenJDK as part of Project Lilliput. It is a feature that reduces the size of object headers in the HotSpot JVM to 64 bits on 64-bit architectures. This helps to reduce heap size, especially in applications that create lots of small objects, CPU time, and improve deployment density. The goal of the feature is to get better memory efficiency without changing Java code or object behavior. JEP 534 makes compact object headers the default in the HotSpot JVM. JEP 536: JFR In-Process Data Redaction JEP 536 adds a new feature to Java Flight Recorder — in-process data redaction. This means that JFR can hide sensitive startup data, like command-line arguments, environment variables, and system properties, before the recording leaves the running JVM. And it does it by default, without any additional configuration. This is a very useful and log-needed feature because JFR files are often shared with support teams or stored in bug reports, and nobody wants a password accidentally preserved in a performance recording! If an application needs the old behavior, however, redaction can be configured or turned off with JFR options. Improved Features JEP 531: Lazy Constants (Third Preview) Lazy constants are objects holding immutable data. Like final fields, they are treated as constants by the JVM and allow for the same optimizations. But unlike finals, they don’t have to be initialized at the moment of creation. It allows the application to initialize incrementally and so, start faster. JEP 531 re-previews the feature with several significant changes: Remove the low-level methods isInitialized() and orElse() to keep the API simpler and harder to misuse. Add a new factory method Set.ofLazy(...). This means you can define a fixed set of possible elements, but compute whether each one is actually included only when it is checked. This is a preview feature so you have to enable preview features to use it. JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview) Primitive Types in Patterns, instanceof, and switch make Java pattern matching work with primitive types like int, long, double, and boolean. That means instanceof and switch can now reason about primitive values more directly. For example, a switch can match an int value using type patterns and guards. It makes numeric checks cleaner and more expressive. JEP 532 re-previews this feature without change. Enable preview features to use it. JEP 533: Structured Concurrency (Seventh Preview) Structured concurrency is a feature that aims to improve the experience of working with concurrency in Java. The idea is to treat several related tasks as one unit, so they start together, finish together, and fail in a more predictable way. This is especially useful when one request needs several things in parallel, like loading a user, orders, and recommendations at the same time. JEP 533 re-previews the feature with several changes. The main change is in the exception handling. The built-in joiners now throw the familiar ExecutionException, the same kind of exception Java developers already know from Future.get(). This is a preview feature so you have to enable preview features to use it. JEP 537: Vector API (Twelfth Incubator) Vector API provides a way to write complex vector algorithms directly in Java code. Developers can use a programming model that will enable reliable and efficient vectorization. Manually written vector loops can express high-performance algorithms, which an auto-vectorizer may never optimize. This feature will be useful in various areas, including machine learning, linear algebra, cryptography, finance, and code within the JDK. The Vector API was first introduced in JDK 16. JEP 529 re-incubates the Vector API without significant changes. The long-term goal of the Vector API is to take advantage of Project Valhalla's features. This project aims to improve the Java object model. So, Vector API will incubate until necessary features of Project Valhalla become available as preview features. After that, Vector API will be promoted from incubation to preview. JEP 538: PEM Encodings of Cryptographic Objects (Third Preview) PEM Encodings of Cryptographic Objects is a new standard Java API for reading and writing files in the Privacy-Enhanced Mail transport format. This format is often used for cryptographic keys, certificates, and certificate revocation lists. JEP 538 re-previews this API with a few changes like the addition of classes and methods, renaming of classes and making a PEM class an ordinary class instead of a record. As it is in preview, you need to enable preview features to try it. Conclusion Early-access builds of JDK 27 are available so you can experiment with them now or wait for the GA of your favorite distribution in September. - [Top-8 Issues of AI-Driven Development and How to Keep Them Out of Production](https://bell-sw.com/blog/top-8-issues-of-ai-driven-development-and-how-to-keep-them-out-of-production/): AI coding is not the problem. Blind trust in AI coding is the problem. And oh, how tempting it is to trust! The output is clean. It's confident. It even has comments. It looks like it was written by someone experienced who knew exactly what they were doing. But that is the trap, and plenty of developers have already burned themselves on it. So let's go through the eight ways AI-generated code turns from a productivity boost into debugging debt, security exposure, and production chaos. And also, what to do about each one. Table of Contents 1. The Trust Gap: “Almost Right” Code is Worse than Wrong 2. Security Vulnerabilities in Generated Code 3. Supply Chain Risk: Hallucinated Dependencies 4. Untested AI Code Reaching Production 5. Privacy, IP, and Secret Leakage 6. Agentic Tooling as a New Attack Surface 7. Governance Chaos and Skill Erosion 8. Missing Context in Real Codebases So, should you stop using AI to code? 1. The Trust Gap: “Almost Right” Code is Worse than Wrong The question stopped being can AI write code a long time ago. Of course it can. The question every developer should be asking is: can I trust this code? Stack Overflow's 2025 survey found that 66% of developers are frustrated by AI solutions that are "almost right, but not quite." Almost right is the dangerous part. Completely wrong code announces itself. It just won't compile.But what does “almost right” mean? In an enterprise system, "almost right" means The happy path works but the edge cases fail; The timeout handling on a perfectly valid API call does the wrong thing; The concurrency code passes tests locally and then falls over the first time real traffic hits it. But: the work moves on, and the bug just waits. Which is why developers now spend their time on the unglamorous part: reading generated code line by line, reproducing the problem, checking whether a dependency actually exists, inspecting whether a generated test tests anything at all. Then asking the model to redo it, or redoing it themselves. ITPro reports that 45% of developers lose time debugging AI-generated code, and a METR study of experienced open-source developers found that AI assistance actually made them slower, even though they felt faster. That's the dark side of the trust gap: you feel quicker, but in reality, it is not like that. The fix starts in the developer's head, before any tooling gets involved. That is: distrust AI code by default. However polished it looks, route it through real testing and vulnerability checks. And do the planning up front. Define your requirements, constraints, and goals before the model writes a line, in the spirit of spec-driven development. Hand it a detailed execution plan to follow rather than one bulk "go build it" prompt, and you keep the model inside boundaries you chose instead of cleaning up after the ones it invented. 2. Security Vulnerabilities in Generated Code Buggy code is one problem. Insecure code that compiles, passes every test, and ships with a bunch of vulnerabilities, is a worse one. Veracode's 2025 GenAI Code Security Report found that AI-generated code introduced security flaws in 45% of tests, and no major language came out clean. Java had a particularly rough showing, with over 70% of outputs in their task set landing as vulnerable. Weak defaults, unsafe input handling, poor authentication, SQL injection, insecure serialization, vulnerable dependencies… A little petshop of horrors, I would say. But wait, it gets worse! A 2026 study on AI coding assistants found that developers relocated security from "write secure code" to "review generated code later." In observed sessions, participants didn’t put security requirements in their initial prompt, even when they knew the risks. So the productivity gain becomes a vulnerability pipeline: generate fast, review later, miss something, patch under pressure, repeat. Run that loop long enough and one of the misses becomes a data breach. Security has to be a design-time decision, taken even before the model writes anything. Put your security requirements into the prompt itself. The OWASP Secure Coding Practices checklist is a good starting catalog. Then run SAST, SCA, and secret scanning over whatever comes back, and require a human security review for anything sensitive. Build on a base you trust to receive timely security patches, preferable, hardened base images as they come with tighter security, built-in provenance, and low-to-zero CVEs by default. BellSoft Hardened Images are a solid, fully open-source choice for that. The model doesn't know your threat model. It doesn’t make you less accountable. 3. Supply Chain Risk: Hallucinated Dependencies AI does not only hallucinate code. It can also hallucinate dependencies. That quirk has spawned an entire attack class called slopsquatting. The mechanics are simple. The model confidently suggests a plausible-sounding package that doesn't exist. An attacker registers that name and ships it with a malicious payload. In this scenario, the attacker doesn't have to break your code at all. They just wait for your toolchain to trust a name your AI made up. Trend Micro has a good write-up on how this plays out in practice. The long game is poisoned automation. A coding agent generates the dependency, CI installs it, scanners don't catch the mismatch, and the malicious package gets normalized inside your build. After that, the attacker is helping themselves to credentials, environment variables, build secrets, and quite possibly your source and infrastructure. Therefore, never install an AI-suggested dependency on faith. Before anything enters your tree, verify that the package exists, and also who maintains it, its provenance, release history, license, vulnerability status, and whether it belongs in your approved catalog. Lean on SCA, lockfiles, SBOMs, provenance checks, and dependency allowlists, and isolate the install in a sandbox so only validated artifacts make it through. Because as it turns out, a package name is the easiest thing in the world to fake. 4. Untested AI Code Reaching Production You wouldn't push human-written code to production without testing it. Apparently a lot of teams feel differently about the machine-written code. Tricentis' 2026 Quality Transformation Report says 60% of organizations ship untested AI-generated code, and 32% do it deliberately because executives are pushing for speed. Meanwhile, GitClear's 2025 research found a sharp rise in copy-pasted and cloned code, including 4x more cloning than before the AI boom. The short-term result is more bugs and security gaps. The long-term result is the one that costs money. A codebase full of duplicated logic, inconsistent style, weak abstractions, and missing tests is a codebase nobody wants to refactor. But eventually, somebody has to! So, the bill arrives with slower releases, fragile changes, higher incident rates, and a quarter of your engineering capacity spent cleaning up the vibes. The remedy is boring and effective: make AI code earn its way to production through the same gates everything else does. Define CI quality gates with linting, static analysis, type checks, unit, integration, and security tests, and scan the finished artifacts for known CVEs so critical ones don't ride along into prod. 5. Privacy, IP, and Secret Leakage There's a quieter risk that has nothing to do with whether the code works: where your code goes while the assistant is helping you write it. Developers paste proprietary source, stack traces, logs, and even secrets into tools whose data handling isn't always clear enough for an enterprise risk model. Personal AI accounts make it worse, because now company code is flowing through services that sit entirely outside your retention policies and audit controls. A 2025 privacy scorecard found broad opacity across AI coding assistants: opt-out-by-default training, and a near-universal failure to proactively filter secrets out of prompts. Netwrix went further and found credentials stored as plaintext JSON in predictable local paths. And SC World reported GitGuardian findings that public GitHub secret leaks rose 34% in 2025, with AI-assisted commits twice as likely to leak a secret. Then there's the licensing side. Sometimes generated code closely matches existing public code, which drags in attribution and IP questions. That public snippet might be GPL, Apache, or MIT, each with its own obligations. Plus, it might carry the original's bugs and vulnerabilities along for the ride. So, lock the perimeter. Use approved enterprise tools only, block secrets in prompts, and forbid personal AI accounts for proprietary code. Run secret scanning locally and in CI, scan your git history, rotate anything that leaked, and reference vaults or environment variables instead of hardcoding credentials. Configure your assistant to flag or block suggestions that match public code, and read what it pulls in before you accept it. 6. Agentic Tooling as a New Attack Surface Agentic tools have outgrown autocomplete. They read files, run commands, install packages, call other tools, and edit your repo on your behalf. More work handed to the agent is genuinely great, but only until we remember that it also means a bigger blast radius. And we are talking not only about the wiped out production database. It also makes prompt injection a real system action. A 2026 paper describes how hidden instructions tucked into external artifacts can hijack a coding assistant and turn it into, in their words, an "attacker's shell," running unauthorized commands with the developer's own privileges. The reported attack success rates ran from 41% to 84% across tested payloads, with assistants hunting for credentials, rewriting authentication config, and exfiltrating data. The IDE is exposed, too: Tom's Hardware covered the IDEsaster research, which uncovered over 30 critical AI IDE flaws enabling data theft and remote code execution. This is what happens when your IDE becomes both the assistant and the attack surface. The OWASP AI Security Verification Standard states the problem explicitly: the AI coding agent is an actor in your supply chain, with an identity and authority of its own. It can act on its own behalf, or be acted upon by an attacker. AISVS recommends written policies for when AI tools may generate, refactor, or review code, covering the whole SSDLC from design through deployment and monitoring. Give the agent the same scrutiny you'd give a new hire with production access. Because that's what it is. 7. Governance Chaos and Skill Erosion Some of these risks will never show up in a scanner. This is because they're organizational, and therefore, harder to notice or mitigate. JetBrains' State of Developer Ecosystem 2025 reports that 68% of developers expect their employers to require AI-tool proficiency. At the same time, they worry about their own skills eroding. Forced adoption compounds the issue, because now agentic AI is one more distributed system to govern, observe, secure, budget, and debug. Teams start optimizing for "AI usage" as if it were the goal, instead of the things that have always been the goal: reliability, maintainability, security, performance, correctness. Down that road lie cognitive overload, surprising AI bills, dulled engineering instincts, and policies that lag behind what developers are actually doing. And there's a generational trap forming. Companies hesitate to hire juniors because they expect AI to cover that work. Juniors who do get hired lean on AI so heavily that they don't build the instincts seniors are made of. So where, exactly, are the next seniors coming from? That one we get to find out the hard way. Alas, there's no clear fix for skill erosion. There are no best practices or a checklist to follow. But governance you can do. Write an AI coding policy that names approved tools, review requirements, attribution and traceability, ownership, training, logging, and cost controls; the NIST AI Risk Management Framework is a solid backbone for it. The core discipline is refusing to treat "AI usage" as a success metric, and continuing to measure code quality, reliability, security, and maintainability the old-fashioned way. Whatever else changes, you still own the code you shipped, even the parts you didn't type. 8. Missing Context in Real Codebases AI is brilliant at isolated problems and clean little demos. Fortunately or not, your production system is neither. Real codebases come with conventions, weird build logic, and undocumented constraints. The model doesn't know why the code is shaped the way it is. It doesn’t know that one branch handles a customer-specific edge case, or that a migration got abandoned half-finished three years ago, or that there's a method nobody dares refactor because the whole app can go down. So, the AI tool suggests a change that looks perfectly reasonable in isolation, but violates an architectural rule, a domain invariant, a compatibility guarantee, or a performance assumption nobody documented. Over time that produces architectural drift: more inconsistent patterns and less shared understanding of how the system fits together. It also thins out ownership, because changes can land without anyone fully grasping how they interact with everything around them. The bigger and older the codebase, the more dangerous context-light automation gets. The answer here is less prompt engineering and more context engineering. Maintain explicit context for the tool with requirements, tests, architecture notes, repository instructions. Create a dedicated context file that spells out the relevant quirks of your codebase, and that the assistant reads before it touches anything. This file might do more for output quality than any amount of prompt sorcery. So, should you stop using AI to code? No. I'm not here to talk you out of AI-assisted development, and I'm not going to pretend the safe move is to switch it off. What I’m trying to convey is a simple idea: stop trusting it blindly! AI-generated code isn't automatically dangerous. It's automatically unverified, and unverified code earns its place the same way everything else does, whoever or whatever wrote it: review, test, scan, isolate, and own the result. So, set up the guardrails before the generated code bites you: Wire real quality gates into CI: linting, static analysis, type checks, the full test suite; Run SAST, SCA, and secret scanning over everything the model hands you; Scan your built artifacts for CVEs. None of this is new. It's the discipline we already apply to human code. The eight problems above are the dark side. The engineering practices that keep that side in check should be the bright side, and a good place to start writing your own guardrails. - [What Are Hardened Container Images? Benefits, Security, and Adoption](https://bell-sw.com/blog/what-are-hardened-container-images-benefits-security-and-adoption/): Container images are the foundation of modern application delivery, but many teams build containers that contain far more software than their application needs. This is because a typical base image may include package manager, download utilities, debugging tools, libraries, and operating-system components not required in production. The problem is that each unnecessary component increases the attack surface and makes security scanning harder to manage. Hardened container images are designed to change that as they provide a minimal, security-focused foundation for running applications. This article explains what hardened container images are, how they differ from standard images, and where they fit into a modern container security strategy. It also uses BellSoft Hardened Images as an example of how organizations can build and run Java and other workloads on a smaller, signed, traceable, and continuously maintained container base. Table of Contents Why Traditional Container Images Create Security Risks What Are Hardened Images? Hardened images vs slim, distroless, and scratch Benefits of Hardened Images Smaller attack surface Less scanner noise Predictable patching cadence Clear supply-chain controls Facilitated compliance work Consistent secure defaults What Are BellSoft Hardened Images? Getting Started with Hardened Images Step 1: Select a hardened image Step 2: Change the Dockerfile Step 3: Build the image and test it Step 4: Verify the signature and retrieve an SBOM Hardened Images Adoption Checklist Conclusion Why Traditional Container Images Create Security Risks Container images make cloud deployment fast and portable, but ironically, the most convenient part of the software supply chain is often also the weakest one. The Netrise Supply Chain Visibility & Risk Study has revealed that an average container image comes with 604 known vulnerabilities, almost half of them years old. This isn’t because the engineers are doing their job badly at eliminating the CVEs. This is because container images contain many layers such as OS packages, possibly a language runtime, system libraries, configuration files. Many of the components in these layers are not required by the application running in production, but nevertheless, each layer adds to the attack surface with unpatched packages, unmaintained dependencies, untrusted base images, or misconfigurations. Even worse, new vulnerabilities are discovered constantly, and what was safe yesterday becomes flawed today. As a result, teams spend a lot of engineering resources on constant firefighting in the code they didn’t even write. Some container image components might not contain known CVEs, but introduce security risks by themselves. For instance, a package manager can give the attacker an opportunity to install malicious packages. Hardened images are aimed to solve these issues at the very foundation — the base image. What Are Hardened Images? Hardened images are minimized, immutable container images that contain only essential components for running an application and are continuously patched and shipped with provenance metadata to reduce known vulnerabilities and supply-chain risks. Minimized attack surface is not their only characteristic, albeit a very important one. Here are five factors that differentiate hardened images from standard base images: Minimalism: The hardened base image contains only the components the application requires to run, no spare parts. Immutable component set: There is no package manager or download utilities, so the images cannot be changed externally. Continuous patching: The hardened images have low-to-zero CVEs by design, and the maintainer continuously updates the images with the latest batches to keep the images clean. Built-in provenance data: The hardened images come with provenance data such as a Software Bill of Materials (SBOM) and a cryptographic signature verifying that the software artifact wasn’t altered. Vendor accountability: The vendor owns the base images as a product, offering a commitment to constant patching, a defined release cadence, and response SLAs when critical issues emerge. Area Standard container images Hardened container images Component set Often broad Minimal, purpose-built Package manager, shell, and download utilities Commonly included Typically removed Attack surface Larger Reduced Known CVEs Depends on the vendor; for images with unclear maintenance strategy, can accumulate quickly Continuously monitored and remediated Provenance May be limited SBOMs and signed artifacts Image contents Vary by tag and vendor Controlled and documented Runtime changes Packages may be added Immutable component set CI/CD verification Optional Designed for policy checks Debugging Tools often included Separate debug workflow needed Supply-chain controls Usually external Built in through signatures and metadata Compliance Usually not compliant with regulatory requirements Configured to meet various regulatory requirements (FIPS, STIG, etc.) Given their innate characteristics, hardened images can help teams break the vicious circle of CVE firefighting, stop wasting engineering resources on patching the third-party code such as OS or runtime, and build their infrastructure on a secure and compliant foundation. Hardened images vs slim, distroless, and scratch Before hardened images, there were slim, distroless, and scratch images. How do hardened images differ from them? And how do they differ from each other while we are at it? Container image labels can be confusing. Terms such as slim, distroless, and scratch mainly describe what is included in an image. They do not, by themselves, explain who maintains the image, how quickly it is patched, or whether teams can verify its contents. Scratch is an empty base image. It has no Linux distribution, shell, shared libraries, certificates, or package manager. To use it, you add the application binary and every file it needs yourself. Scratch works well for statically linked Go or Rust binaries. It is usually impractical for Java applications because a JVM needs a runtime and supporting libraries. Distroless images contain the runtime files an application needs. The most minimalistic distroless image contains only ca-certificates, timezone data, /etc/passwd file, and a tmp directory. Some distroless images may contain a libc implementation and selected shared libraries. But all of them exclude a shell and package manager. Slim images are reduced versions of conventional Linux-based images. They remove some packages including the package manager and sometimes a shell, but their exact contents vary by vendor and tag. Hardened images describe a different dimension. Although a hardened image can also be distroless, its definition goes beyond the number of files removed from the final layer. It should have a deliberately limited and immutable component set, documented provenance, signed artifacts, an SBOM, a vulnerability-management process, and a defined update policy. That distinction matters because a small image can still contain outdated packages, come from an unclear source, or leave teams without a predictable path for security fixes. Hardened images address those operational questions as well as image size. Benefits of Hardened Images Hardened images give teams a more controlled starting point for container security by reducing avoidable risk and facilitating compliance. Smaller attack surface Removing tools such as wget, package manager, compiler, and debugger limits what an attacker can do. Without those tools, it is harder to inspect the environment, install software, or fetch additional payloads. Less scanner noise Smaller images contain fewer packages and utilities, so they usually generate fewer CVE findings. That helps development and security teams focus on issues that affect the application instead of triaging vulnerabilities in components not used by any service. Predictable patching cadence A hardened-image vendor maintains the components included in its images and publishes updates when vulnerabilities are found. Instead of reactively patching operating-system packages and runtime dependencies one by one, teams can move to an approved base image with a documented release schedule and remediation SLA. Clear supply-chain controls SBOMs, image signatures, and provenance metadata show where an image came from, what it contains, and whether it has changed. Teams can use that information to enforce CI/CD policies and demonstrate that production workloads run approved artifacts. Facilitated compliance work Many regulations, for example, EU Cybersecurity Act or the U.S. President’s Executive Order No. 14028 on Improving the Nation’s Cybersecurity, require an accurate software inventory, vulnerability management, and oversight of third-party dependencies. Hardened images simplify that work directly by providing the required compliance data and indirectly by reducing the number of included components and making image contents easier to document and review. Consistent secure defaults Platform teams can standardize on a set of hardened base images for projects instead of leaving every team to choose public images or build Dockerfiles from scratch. This reduces variation between services and makes security practices easier to apply across the organization. It is important to understand that hardened images do not magically solve all issues with enterprise container security. You still need to implement reasonable security measures such as not running privileged containers or giving them excessive capabilities. Nor do hardened images magically rebuild, re-test, and redeploy your services when a patch becomes available. But what they really do is give teams a smaller, traceable, and easier-to-maintain foundation, leaving more attention for the application code and infrastructure they are responsible for. What Are BellSoft Hardened Images? Several vendors provide hardened container images built around similar principles: reduce unnecessary components, track known vulnerabilities, publish software metadata, and rebuild images when security fixes become available. BellSoft Hardened Images follow this model as well, offering images for Java, GraalVM Native Image, Python, Go, C/C++, and Node.js. Each image includes an SBOM and is cryptographically signed, allowing teams to inspect image contents, verify artifact integrity, and enforce provenance checks in CI/CD pipelines. Some distinctive features of BellSoft Hardened Images include: “3-in-1” approach to security. The images are designed around a stack that BellSoft maintains themselves across the operating system, language runtime, and container image layers. It means that one team covers the OS, runtime, and container security under one SLA and can proactively create patches for known CVEs without having to wait when the patch appears upstream. This allows the teams to standardize on a chosen set of images without having to manage several vendors. Focus on compatibility. The images are based on Alpaquita Linux, a lightweight Linux distribution designed for cloud and container workloads and available with either musl or glibc, with AMD64 and ARM64 support. This gives teams a smooth migration path for existing workloads without requiring them to rework the application to adopt a new base image. Multiple delivery channels. The images are available on Docker Hub, GitHub Container Registry, Microsoft Container Registry, Google Container Registry, and Amazon ECR for seamless integration into the existing infrastructure. Flexible plans. BellSoft Hardened Images are open source and free to use in production. Commercial plans add remediation commitments, including a seven-day SLA for critical CVEs and a 14-day SLA for high, medium, and low severity vulnerabilities. Getting Started with Hardened Images In many cases, migration to hardened images should be issue-free. The work is in checking the assumptions that the old image supported and developers relied on. The image intentionally omits tools that developers often assume are available: package manager, download utilities, and debugging commands. Distroless BellSoft hardened images also don’t contain a shell. It means that any step that relies on these components, such as downloading a file at startup or installing a package inside a running container, belongs in the build process or in a separate operational workflow. Step 1: Select a hardened image Start with one service that has a simple runtime profile. Choose the hardened images for your language and the libc variant, musl or glibc. For Java applications, a musl-based image is smaller and works well for many self-contained services. A glibc-based image is often the safer choice when the application depends on JNI, Java agents, native libraries, or third-party binaries built against glibc. Step 2: Change the Dockerfile A basic migration may look like this. Here’s a Dockerfile for creating a Java container image on a general-purpose base: FROM eclipse-temurin:25-jdk as builder WORKDIR /app COPY . /app/app RUN cd app && ./mvnw package EXPOSE 8080 ENTRYPOINT ["java","-jar","/app/app/target/*.jar"] Update the base image in the Dockerfile. You can keep it as single-stage, but it is strongly recommended to use Docker multi-stage builds to keep the final image as clean as possible. FROM bellsoft/hardened-liberica-runtime-container:jdk-25-musl as builder WORKDIR /app COPY . /app/app RUN cd app && ./mvnw package FROM bellsoft/hardened-liberica-runtime-container:jre-25-nonroot-musl WORKDIR /app COPY --from=builder /app/app/target/app-*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","/app/app.jar"] The base image used for the second stage includes a nonroot tag, which means that the container will run as nonroot, so you don’t have to specify the non-root user manually. Step 3: Build the image and test it Build the image with the hardened base, run it locally, and use the same tests and deployment checks as for a normal release. The application code and startup command stay the same. The main change is the runtime base: a minimized, immutable image with fewer components to scan and maintain. In case you are used to having diagnostics tools for debugging, use a separate ephemeral troubleshooting container. The same goes for profiling. Step 4: Verify the signature and retrieve an SBOM Hardened images come with a cryptographic signature and an SBOM, so in the production pipeline you should verify the signature and attestations before building a container image to make sure that this is exactly the image you have chosen. You can use an open source tool Cosign to verify the image and retrieve an SBOM in one go. Download and save the BellSoft public key and use the following command: $ IMG='docker.io/bellsoft/hardened-liberica-runtime-container:jre-25-nonroot-musl' cosign verify-attestation \ --key ~/keys/cosign-bellsoft.pub \ --type cyclonedx \ $IMG | jq -r '.payload' | base64 -d | jq '.predicate' Where ~/keys/cosign-bellsoft.pub is the path to the public key provided by BellSoft and --type is the type of SBOM (both spdxjson and cyclonedx are supported). This command should print out the SBOM content to the standard output. Hardened Images Adoption Checklist A hardened image works best when the build and delivery process is prepared for it. Replacing the FROM line is usually easy. Discovering that an old startup script relies on curl, bash, and a package manager is bound to introduce some friction at the start. Also, an update strategy is still needed because if the vendor updates the image with the latest patches, and you rebuild the services once a year, the low-to-zero CVE promise will not hold out for long. Therefore, use this checklist when moving services to hardened base images. Action item Clarification List runtime dependencies Check for native libraries, Java agents, shell scripts, and external binaries. Anything the application needs at runtime must be present in the final image. Remove package installation at startup Do not install utilities or dependencies when the container starts. Add required files during the build, or create a derived image when a dependency belongs in production. Choose the libc variant Use musl when the application has no compatibility issues and the tests show improved performance. Use glibc when the workload depends on native components built for glibc, or when testing shows that it is the safer option. Use multi-stage builds Use a multi-stage Docker build and copy only the final JAR, binary, or static assets into the hardened runtime image. Run a non-root user Use a non-root base image or set an explicit user in the Dockerfile. Verify that the application can write only to the directories it needs, such as a temporary-data or log directory. Configure the ports Run the application on a non-privileged port, such as 1025 or above. In Kubernetes and Docker Engine releases before 20.10, a non-root process cannot bind to ports below 1024 inside the container. Pin image digests Use digests in production deployments instead of tags. A digest identifies the exact artifact that passed testing; a tag can later point to a different one. Decide how debugging will work Keep diagnostic tools out of the production image. Use a dedicated debug image, an ephemeral container, or cluster-level observability tools when troubleshooting is required. Verify images in CI/CD Check signatures, inspect and retrieve SBOMs, and apply policy checks before deployment. This confirms that the image came from an approved source and contains the components you expect. Assign ownership for base-image updates Decide who monitors hardened image releases, tests updated images, and approves rollout. A remediation SLA only helps when you rebuild and deploy the patched image. Conclusion Hardened images are not a substitute for secure code, runtime controls, or vulnerability management. But they work at the foundation, removing unnecessary software from the base layer and making the remaining components easier to verify, patch, and maintain. BellSoft Hardened Images give Java and other production workloads a maintained base with signed artifacts, SBOMs, and musl or glibc variants. BellSoft maintains both the Linux layer and the runtime, so teams are not left stitching together updates from separate vendors when a vulnerability lands. The sensible way to evaluate them is to start with one service. Run it through the existing build and deployment pipeline, then compare the image contents, scanner output, and debugging experience with the current base image. If you have any questions regarding the hardened images, including the migration and customization, our engineers will be happy to help. Contact us - [What Are Buildpacks? Build OCI Images Without Dockerfiles](https://bell-sw.com/blog/what-are-buildpacks-build-oci-images-without-dockerfiles/): Buildpacks turn application source code into OCI container images without requiring every team to maintain a Dockerfile. They detect supported applications, supply runtimes and dependencies, produce reusable layers, and expose build metadata such as SBOMs. This article covers how buildpacks function, what their benefits are as compared to Dockerfiles, and how to use them with various programming languages, including Java, Python, Go, and Node.JS. Table of Contents How Buildpacks Work Benefits of Buildpacks Enhanced Developer Productivity Consistent Images Across Teams Better Caching and Faster Rebuilds Centralized and Fast Runtime Updates Language-Aware Builds Customization Stronger Security Defaults Buildpacks vs Dockerfiles A Guide to Using Buildpacks How to Use Buildpacks with Java How to Use Buildpacks with Python How to Use Buildpacks with Go How to Use Buildpacks with Node.js Retrieving a Software Bill of Materials Conclusion How Buildpacks Work A buildpack is a software that turns the application source code into a runnable production-ready container image. To build a container image of your application, you don’t need to install the runtime, compile the application beforehand, or configure the build environment. The buildpacks will handle everything. But how? In short, buildpacks detect what kind of application you have and what it needs to build and run. Then, they execute a build using a set of components required for your app. But that is a short story, and the beginner’s tutorial on using buildpacks will be quite short, just two commands. But in truth, there’s quite a lot going on under the hood to make this ‘magic’ happen. First, we need to distinguish several parts of the concept. A buildpack is not just one universal tool, but a set of software solutions: A buildpack knows how to recognize and gather everything an application needs to build and run. A builder packages buildpacks, lifecycle tooling, and build/run-image configuration. The builder stack consists of two images: the build image and the run image. The build image provides the build environment, which is a containerized environment where buildpacks are executed, the run image offers the environment for the application image during runtime. A buildpacks platform executes the buildpack lifecycle. It supplies source code, buildpacks/builder, cache, registry credentials, environment variables, then invokes the lifecycle and handles the resulting image. In this article, we will use pack CLI as the buildpacks platform. A lifecycle coordinates the build and exports an OCI image. It executes the actual CNB build phases: Analyze, Detect, Restore, Build, Export. Let’s look at these phases in more detail. The analyze phase During the analyze phase, the lifecycle checks whether an earlier version of the target image exists and reads its metadata. It also verifies that it can write the new image to the target registry. The detect phase During the detect phase, a group of buildpacks is tested against the source code, and the first group deemed fit for the code is selected for building the app. After the buildpack detects the necessary indicators, it returns a contract of what is required for creating an image and proceeds to the next phase. The restore phase During the restore phase, the lifecycle uses the previous-image metadata, cache, and selected buildpack group to restore reusable layers. Restored layers are placed in the layers directory. This way, buildpacks can reuse them instead of downloading dependencies or rebuilding unchanged artifacts. The build phase During the build phase, the buildpack transforms the codebase, fulfilling the contract requirements composed earlier.The lifecycle runs the selected buildpacks’ build executables in order, passing each one the part of the build plan relevant to it. Buildpacks create layers for runtimes, dependencies, compiled application output, environment configuration, launch processes, and SBOM data. The export phase During the export phase, the lifecycle assembles the final OCI image from the run image, application source, and layers marked for launch. It sets the image entrypoint, writes image metadata, and stores cacheable layers for later builds; cache-only layers stay out of the final runtime image. Benefits of Buildpacks Cloud Native Buildpacks take away most of the plumbing development and engineering teams have to go through on a regular basis when working with Dockerfiles. When dealing with Dockerfiles, developers need to define the whole process of building a container image: choose base images, install the dependencies, compile the application, define the entrypoint and startup commands, ideally, optimize for performance and security. For platform teams, Dockerfiles pose more of a maintainability problem, when a platform team may need to update dozens of Dockerfiles, each written differently, test them all, and ping teams for merges. Also, standards are harder to enforce in this scenario. Buildpacks take away most of this burden. Not in a sense that they let you sweep image security, performance, or maintenance under the rug. Those concerns still exist. Instead, buildpacks move common container-building logic into reusable, language-aware components managed centrally. Developers provide application code and configuration, and buildpacks create a production-ready image with best practices of security and efficiency in place. Platform teams can maintain shared builders and buildpacks rather than repairing every repository’s Dockerfile independently. That brings several advantages. Enhanced Developer Productivity A developer can package an application without becoming an expert in Dockerfile syntax, layer ordering, base-image selection, or multi-stage builds. That matters more for medium-to-large enterprises, when dozens of teams deploy fairly standard services but do not need to customize every OS package or build command. The platform team can maintain a supported builder, while application teams spend their time on application code and configuration rather than reinventing Dockerfile best practices. Consistent Images Across Teams Buildpacks apply the same defaults for supported applications: runtime installation, dependency resolution, launch configuration, image layering, and, depending on the builder, non-root execution. This helps to reduce the usual chaos where every repository has its own Dockerfile dialect, base image, and workarounds. Platform teams can standardize runtimes, build settings, and image policies through the builder instead of reviewing and repairing each service separately. Better Caching and Faster Rebuilds Buildpacks split an image into layers for things like the runtime, dependencies, and application code. When only the application changes, dependency layers can often come from cache instead of being downloaded and rebuilt. That usually speeds up local and CI builds and cuts unnecessary registry storage and network traffic. The exact result depends on the buildpack and the application, but the cache strategy is built into the process rather than left for every Dockerfile author to design from scratch. Centralized and Fast Runtime Updates Buildpacks keep application layers separate from the runtime base image. When the run image receives an update, such as an OS security patch, you can replace those base layers without rebuilding the application or downloading its dependencies again. That process is called rebasing. For a compatible buildpack-built image, rebasing is much faster than a full rebuild. This advantage becomes even more evident when several applications use the same run image and an urgent operating-system patch lands. The application code and layers created during the build stay as they are. Only the run-image layers change. When a runtime, build dependency, or base image needs an update, the platform team can update the relevant buildpack or builder rather than opening pull requests across every repository. Language-Aware Builds Buildpacks understand the conventions of supported ecosystems. They can inspect an application, select the matching buildpacks, install the required runtime, resolve dependencies, compile the code when needed, and configure how the application starts. For common stacks, this removes a lot of Dockerfile boilerplate. It also makes it less likely that teams use different build patterns. Customization A builder is made up of smaller buildpacks. Platform teams can add, remove, replace, or reorder buildpacks to meet their own requirements. They can also create their own buildpack. That gives teams an optimal middle ground in-between total freedom of Dockerfiles and buildpack-promoted standardization. They are not locked into a completely fixed build process, but they also do not have to maintain a custom Dockerfile for every service. For standard applications, that is often enough flexibility. Stronger Security Defaults Buildpacks can make secure choices easier to standardize. Instead of every team maintaining its own Dockerfile, a platform team can define approved runtimes, base images, package sources, certificates, and user settings in shared builders and buildpacks. Applications containerized with buildpacks also run as non-root by default, which limits the damage if a process is compromised. Updates become more controlled as well. When a runtime or base image needs a fix, maintainers can update the shared build path instead of asking every team to edit a Dockerfile. Images still need to be rebuilt or rebased, but the work becomes a centralized platform operation. Buildpacks can also generate SBOMs, making it easier to see what ended up in an image and use that data for scanning, audits, and incident response. Buildpacks vs Dockerfiles Buildpacks and Dockerfiles represent two approaches to building application container images. The goal of this article is not to paint Dockerfiles black. In some cases, a Dockerfile might be more suitable for your requirements. Dockerfiles give you fine-grained control over the build process and thus, ultimate customization. You specify which libraries, dependencies, packages, and configuration are needed for your application, which might be paramount for specific, non-standard scenarios or complex workloads. Buildpacks take away some of these customization capabilities, but in turn, give you standardization across services, which otherwise is hard to reach and maintain manually. In summary, the differences between buildpacks and Dockerfiles can be described as follows. Feature Dockerfile Buildpacks Build approach Explicit Automated Control Full control over every layer Configuration within supported patterns Best for Custom, legacy, unusual workloads Conventional supported applications Languages Any language or stack Supported languages and frameworks Base image Chosen and maintained per app Defined centrally by the builder/run image Dependencies Installed manually Detected and installed automatically Build commands Written and maintained manually Supplied by language-aware buildpacks Caching Depends on Dockerfile design Dependency-aware layers by default Image size Fully tunable, but manual work Depends on builder and buildpacks Security defaults Team must implement them Shared defaults, including non-root execution Runtime updates Update Dockerfiles across repositories Update shared builder or buildpacks Rebasing Rebuild image after base-image change Replace compatible run-image layers SBOMs Add separate tooling Generated and attached during the build Consistency at scale Varies between repositories Standardized across supported apps Customization Unlimited Modular, but bounded by buildpack support A Guide to Using Buildpacks To demonstrate how buildpacks work under the hood, we will use Paketo buildpacks as an example, an open-source project that implements Cloud Native Buildpacks specifications and supports the most popular languages. We will also use pack CLI as the buildpacks platform. Prerequisites: pack CLI Docker A functioning application, which can be as simple as “Hello World!” The setup is the same regardless of the application language. Install pack CLI for your operating system. For Linux and macOS, you can use Homebrew: brew install buildpacks/tap/pack For Windows, you can use Chocolatey or Scoop. For instance, with Scoop: scoop install pack You can also run pack in a container. For other installation methods, refer to the documentation. Make sure that Docker or a Docker-compatible daemon is up and running because pack needs it for running the build process in isolation. For the application, you can use your existing program, write a simple demo, use samples provided by buildpacks or written for this tutorial. For the latter case, run: git clone https://github.com/code-with-bellsoft/buildpacks-demo.git In the following section, I will refer to these samples. The most basic command for building a container image will look like this: pack build --path --builder Where --path points to the source directory, and --builder specifies a builder. Multiple builders from various providers are available. In this tutorial, I will use one of the standard Paketo builders based on Ubuntu Noble, and the BellSoft’s builder based on minimalistic Alpaquita Linux. You can inspect the selected candidate locally: pack builder inspect That shows the included buildpacks, lifecycle version, build image, and run image. A builder is not merely “a list of languages.” It bundles the ordered buildpacks plus the build and runtime base images, so the OS stack and architecture support matter too. Instead of specifying a builder every time, you can set a default builder like this: pack config default-builder Then, the command for building a container image may be as simple as pack build --path How to Use Buildpacks with Java Clone the repo with the samples or prepare your own app. To build an image from source with the standard Paketo builder, run pack build hello-java --builder paketobuildpacks/ubuntu-noble-builder --env BP_JVM_VERSION=25 --path buildpacks-demo/hello-java Alternatively, you can use BellSoft builder with Liberica JDK Lite optimized for cloud and lightweight Alpaquita musl that comes with two libc flavors, optimized musl and glibc. Using this builder may help you reduce the final image size. To build a Java container image with musl-based Alpaquita, run: pack build hello-java --builder bellsoft/buildpacks.builder:musl --env BP_JVM_VERSION=25 --path buildpacks-demo/hello-java To build a Java container image with glibc-based Alpaquita, run: pack build hello-java --builder bellsoft/buildpacks.builder:glibc --env BP_JVM_VERSION=25 --path buildpacks-demo/hello-java To run the containerized application with Docker, use the following command: docker run --rm -p 8080:8080 -e PORT=8080 hello-java To verify that the application has started successfully and listens on localhost:8080, run: curl -i http://localhost:8080 HTTP/1.1 200 OK Date: Tue, 30 Jun 2026 09:34:22 GMT Content-type: text/plain; charset=utf-8 Content-length: 15 Hello from Java Popular Java frameworks such as Spring support buildpacks out-of-the-box, so you don’t even have to install the buildpack platform. To learn how to use and configure buildpacks with Spring Boot, see this article. You can configure the underlying JVM to achieve optimal results for your specific scenario. If you want to use another Java version, use the BP_JVM_VERSION environment variable. For instance, BP_JVM_VERSION=11 will install the newest release of JDK and JRE 11. Also, Paketo buildpacks use Liberica JVM as the Java runtime by default. You can select another available runtime, only check that it provides what you need. For instance, some distributions provide only JDK or a limited choice of Java versions. In addition, you can change the JDK type. The buildpack uses JDK at build-time and JRE at runtime. Specifying the BP_JVM_TYPE=JDK option will force the buildpack to use JDK at runtime. The BP_JVM_JLINK_ENABLED option runs the jlink tool with Java 9+, which cuts out a custom JRE and helps to reduce the final image size: pack build hello-java --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-java --env BP_JVM_VERSION=25 -env BP_JVM_JLINK_ENABLED=true In this case, jlink generates a custom JRE with the following default options: --no-man-pages, --no-header-files, --strip-debug, --compress=1. To change the options, use the BP_JVM_JLINK_ARGS environment variable. If you deploy a Java application to an application server, the buildpack uses Apache Tomcat by default. You can select another server, TomEE or Open Liberty. For instance, run the following command to switch to TomEE: pack build hello-java --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-java --env BP_JVM_VERSION=25 -env BP_JAVA_APP_SERVER=tomee The whole list of JVM configuration options can be found in the documentation for the respective buildpack. How to Use Buildpacks with Python Clone the repo with the samples or prepare your own app. To build an image from source with the standard Paketo builder, run pack build hello-python --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-python To run the containerized application with Docker, use the following command: docker run --rm -p 8080:8080 -e PORT=8080 hello-python To verify that the application has started successfully and listens on localhost:8080, run: curl -i http://localhost:8080 HTTP/1.1 200 OK Date: Tue, 30 Jun 2026 09:34:22 GMT Content-type: text/plain; charset=utf-8 Content-length: 15 Hello from Python You can customize the build using the available options. For instance, you can make the buildpack install the watchexec tool that watches for file changes and run a command when it detects modifications. For that, set the BP_LIVE_RELOAD_ENABLED to true: pack build hello-python --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-python --env BP_LIVE_RELOAD_ENABLED=true For more information about containerizing Python applications and available options, see the documentation. How to Use Buildpacks with Go Clone the repo with the samples or prepare your own app. To build an image from source with the standard Paketo builder, run pack build hello-go --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-go Alternatively, you can use BellSoft builder with lightweight Alpaquita musl that comes with two libc flavors, optimized musl and glibc. To build a Go application locally with the musl-based BellSoft builder, run the following command: pack build hello-go --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-go To build a Go container image with glibc-based Alpaquita, run: pack build hello-go --builder bellsoft/buildpacks.builder:glibc --path buildpacks-demo/hello-go To run the containerized application with Docker, use the following command: docker run --rm -p 8080:8080 -e PORT=8080 hello-go To verify that the application has started successfully and listens on localhost:8080, run: curl -i http://localhost:8080 HTTP/1.1 200 OK Date: Tue, 30 Jun 2026 09:34:22 GMT Content-type: text/plain; charset=utf-8 Content-length: 15 Hello from Go You can customize the build using the available options. For instance, you can enable restarting the binary process when files in the application working directory change. For that, set the BP_LIVE_RELOAD_ENABLED to true: pack build hello-go --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-go --env BP_LIVE_RELOAD_ENABLED=true For more information about containerizing Go applications and available options, see the documentation. How to Use Buildpacks with Node.js Clone the repo with the samples or prepare your own app. To build an image from source with the standard Paketo builder, run pack build hello-node --builder paketobuildpacks/ubuntu-noble-builder --path buildpacks-demo/hello-node Alternatively, you can use BellSoft builder with lightweight Alpaquita musl that comes with two libc flavors, optimized musl and glibc. Using this builder may help you reduce the final image size. To build a Node.js application locally with the musl-based BellSoft builder, run the following command: pack build hello-python --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-node To build a Node.js container image with glibc-based Alpaquita, run: pack build hello-node --builder bellsoft/buildpacks.builder:glibc --path buildpacks-demo/hello-node To run the containerized application with Docker, use the following command: docker run --rm -p 8080:8080 -e PORT=8080 hello-node To verify that the application has started successfully and listens on localhost:8080, run: curl -i http://localhost:8080 HTTP/1.1 200 OK Date: Tue, 30 Jun 2026 09:34:22 GMT Content-type: text/plain; charset=utf-8 Content-length: 15 Hello from Node.js You can customize the build using the available options. For instance, you can enable heap memory optimization. For that, set the BP_NODE_OPTIMIZE_MEMORY option to true: pack build hello-python --builder bellsoft/buildpacks.builder:musl --path buildpacks-demo/hello-node --env BP_NODE_OPTIMIZE_MEMORY=true For more information about containerizing Node.js applications and available options, see the documentation. Retrieving a Software Bill of Materials Software supply chains consist of numerous libraries, tools, and processes used to develop and run applications. A software bill of materials (SBOM) lists all library dependencies utilized to build a software artifact. SBOMs enable the developers to monitor the version of software components, integrate security patches promptly, and keep vulnerable libraries out. When you use buildpacks, an SBOM for your container image is generated by default, so you don’t have to use additional tools or specify the command explicitly. Buildpacks can generate SBOMs in different formats, including Syft, SPDX, and CycloneDX JSON formats. You can use the pack CLI to retrieve an SBOM for a specified image: pack sbom download --output-dir /tmp/demo-app-sbom The SBOM files will be in the specified output directory. You can also download an SBOM of an image in the remote registry using the --remote option. Conclusion Buildpacks give teams a standardized way to turn source code into production-ready OCI images without maintaining a Dockerfile for every application. They handle common work such as runtime selection, dependency installation, image layering, sensible security defaults, SBOM generation, caching, and base-image updates through rebasing. Although applications with unusual operating-system requirements, custom build flows, or unsupported tooling may still need the control a Dockerfile provides, for conventional services, buildpacks can remove a large amount of repeated container work while giving platform teams a more consistent, maintainable, and secure build path. - [Linux in Container in Not the Same as Linux OS](https://bell-sw.com/blog/linux-in-container-in-not-the-same-as-linux-os/): TLDR: "I'm running Ubuntu in Docker" is a lie. What you actually have is a process with Ubuntu OS libraries and an environment on top of the host kernel. Your container's memory limit isn't just your app's memory. Heap, thread stacks, JIT cache, page cache, the garbage collector's own bookkeeping all draw from the same budget, and the kernel is the one holding the OOM trigger. The gap between "a container" and "a real OS" is invisible right up until a CPU throttle, an OOM kill, or a mounted Docker socket turns it into an incident. Table of Contents What a real Linux OS does What you actually get in a container Who draws the walls: namespaces and cgroups Namespaces: the view out the window Cgroups: the meter on the wall Image, container, OS: three things mashed together So why should you care? Where it hurts first: resource limits Where it hurts next: security The short version Ask a room full of developers what's running in their container and most of them will say something like "Ubuntu" or "Alpine" or "Debian." It's a convenient answer, but it's wrong. What you actually have is a process with Ubuntu OS libraries and an environment, running on top of the host kernel. In a perfect world you'd never need to know the difference. The image behaves the same wherever it runs, the app responds, nobody gets paged. But we don't live in that world, and the day something breaks in a way that makes no sense, the difference between "a container" and "a Linux OS" is where the answer might be hiding. So let's be precise about what you actually have. What a real Linux OS does A full Linux OS owns the whole lifecycle of the machine. It starts from a bootloader, brings up the kernel, and hands off to an init system (systemd, on most distros) that starts everything else. From there, it runs everything. It manages hardware and devices, users and permissions, networking, logging, background services, packages, storage, and the security controls around all of it. It also decides who gets what: CPU time, memory, disk, processes. Everything on the machine answers it. Think of it as a house. Applications, system services, the init system, the filesystem, the kernel, the hardware underneath, all of it lives in the same structure and the house owns the plumbing. What you actually get in a container A container isn't a house at all. Think of it as a rented room: it has its own furniture, its own view out the window, its own name on the door, but the building belongs to the host, and so does the foundation. That foundation is the host Linux kernel. In more technical terms, a container is an isolated process environment: it gets a packaged filesystem, its own view of processes and networking and mounts, plus environment variables and resource limits. What it doesn't get is a kernel of its own. It doesn't boot, it doesn't touch hardware, and unless you're running something like an LXC system container, it usually doesn't bother with a full init system like systemd either. The process inside sees its own little world. The host kernel is still running the whole building. Container A Container B Container C Filesystem Filesystem Filesystem Processes Processes Processes Namespaces Namespaces Namespaces ----------------------------------------- Shared host Linux kernel Shared host hardware or VM That shared foundation is the whole trick. It's also where the danger lives, but that's a problem for later in this post. Who draws the walls: namespaces and cgroups If every container shares one kernel, what stops them from wandering into each other's rooms? Two kernel features do the work: namespaces and cgroups. Namespaces handle visibility, deciding what a process is even allowed to see. Cgroups are the meter: they don't care what the process sees, only how much of the host it's allowed to use up. Namespaces: the view out the window A namespace gives a process an isolated view of kernel resources that are still, underneath, managed by the same host kernel. The process thinks it has the place to itself. It doesn't, but the illusion holds. A PID namespace gives the container its own process tree. Inside, your app might be PID 1. On the host it's some unremarkable PID in the thousands. A network namespace comes with its own interfaces, routing table, firewall rules, and ports. That's why localhost inside the container means the container, not the host. A mount namespace hands over a private filesystem layout: image layers, mounted volumes, bind mounts, its own /proc. The host's filesystem stays out of view unless you explicitly let it in. A user namespace maps users inside the container to users outside it, so a process can look like root inside while mapping to an unprivileged user on the host. The UTS namespace isolates the hostname, and the IPC namespace isolates inter-process communication objects like shared memory and message queues. Cgroups: the meter on the wall Cgroups (control groups) are where the metering happens. The container runtime drops the container's processes into a cgroup, and from there the kernel tracks and caps what they use: CPU time (how much the container gets, or how hard it competes with everything else), memory (the ceiling before the kernel starts reclaiming or killing), the number of processes and threads it can spawn, disk I/O bandwidth, and which device files it can open. Anything the kernel can account for, a cgroup can limit. Put namespaces and cgroups side by side against a real OS and the difference gets concrete: Feature Standard Linux OS Linux container Kernel Its own The host's Boot process Full boot A process just starts Init system Usually present Usually absent or minimal Filesystem Real system root Image layers + mounts Processes Whole-machine view Namespaced view Networking Real host/VM stack Virtualized namespace, bridge, overlay, NAT Resource control Whole-OS scheduler cgroup limits Security boundary OS/VM boundary Kernel isolation, weaker than a VM Image, container, OS: three things mashed together There's one more knot to untie, because "container" and "container image" get used as if they're the same thing, and neither is the OS. A container image is a packaged filesystem plus metadata: application files, libraries, binaries, config, certificates, package metadata, sometimes a shell. An Ubuntu-based image, for example, carries an Ubuntu-style filesystem with /bin, /lib, /etc, a package database, and the usual user-space tools. But an image isn't running anything. It has no processes, no CPU or memory use, no IP address. It's a template sitting on a disk. A container is what you get when the runtime starts a process from that image. It takes the image, adds a writable layer on top, sets up namespaces, applies cgroup limits, wires up mounts and networking, and starts the main process. That process becomes PID 1 inside the container's PID namespace, and now you have something alive. So the honest version of "I'm running Ubuntu in Docker" is a bit of a mouthful: I'm running a process from an Ubuntu-based image, using the host Linux kernel, inside an isolated container environment. It's the version to keep in your head, because the short one is exactly what leads people astray when things break. So why should you care? Fair question. If the container starts, the app answers, and nobody pages you at 3 a.m., it's tempting to file all of this under trivia. Here's the catch: the difference between a container and an OS is precisely where a lot of production problems live. They hide at the boundary between the container, the runtime, and the host. And they surface in two places first: resource limits and security. Where it hurts first: resource limits A container's resource limits apply to the entire process tree, not just to your application. Your app, its worker processes, its threads, native libraries, caches, sidecars, and all the runtime overhead share one budget for CPU, memory, I/O, and process count. Memory is where this bites hardest, because the memory limit is a hard boundary the kernel enforces through cgroups. Hit it, and the kernel first tries to reclaim memory. If it can't recover enough, the container goes into an out-of-memory state and the kernel kills one of its processes. The trap is assuming your application data is the only thing using that memory. Every runtime carries overhead you don't see in your own code: Java spreads memory across heap, metaspace, thread stacks, direct buffers, the JIT code cache, native libraries, and the garbage collector's own bookkeeping. With Python you're also paying for interpreter memory, object allocations, extension modules, thread stacks, and allocator fragmentation. Go carries its heap, goroutine stacks, runtime metadata, GC structures, and whatever cgo allocates natively. Databases and proxies lean on caches, connection buffers, background workers, the page cache, and sometimes memory-mapped files on top of all that. So the real equation is: container memory limit = application memory + runtime memory + native memory + thread stacks + filesystem cache + overhead CPU limits play a different trick. A CPU limit doesn't necessarily mean "this container gets one core." More often it means the container runs freely for a bit, then gets throttled once it's used its allotted CPU time. The result is slower requests, longer GC pauses, and latency spikes that show up in your dashboards with no obvious cause. Worse, some runtimes don't even notice the limit: they read the host's CPU count and size their thread pools or parallel GC for a machine they don't actually have. Verify what your runtime detects. Don't trust the defaults. Then there are PID limits. A container can be capped on how many processes or threads it can create, which matters for JVM thread pools, Node.js workers, Python multiprocessing, and web-server worker models. A PID limit protects the host from a fork bomb, but it will also break an app that assumed it could spawn threads forever. If you want to catch these before they catch you, watch the right numbers: memory usage versus the limit, not just usage OOM-kill events and container restart reasons CPU throttling, not only CPU usage process and thread count against the PID limit disk and network I/O where it's relevant application latency and error rate alongside the container metrics docker stats gives you a live view of CPU, memory, network, and block I/O for a quick look. In production, lean on your orchestrator and monitoring stack to line those metrics up against actual application behavior. A good habit: set explicit CPU, memory, and PID limits, then load-test with those limits switched on. That's how you find where the walls are while it's still a test and not a 3 a.m. page. Where it hurts next: security Your container shares the host kernel with every other container and with the host's own processes. That's what makes containers cheap and fast. It's also a shared risk: one kernel bug, one careless privilege, one wrong mount, and a compromised container can reach past its own walls. It can read things it shouldn't, modify host files, take over the container runtime, poke at other workloads, or in the worst case escape isolation entirely. The usual suspects: A privileged container hands over broad access to host devices and a long list of kernel features. Linux capabilities split root into smaller pieces, but the powerful ones (SYS_ADMIN, NET_ADMIN, SYS_PTRACE) still give a container serious reach. Host mounts like /etc, /proc, /sys, /var/lib, or /var/run/docker.sock can expose the host to whatever's inside. Running as root inside the container is a real risk, especially without user namespaces or rootless mode. Bloated images carry outdated packages, vulnerable libraries, unused tools, stray shells, and the occasional leaked secret. The mindset that fixes most of this is boring and effective: reduce what the container can do. Run the app as a non-root user. Never run a privileged container in production! Drop the capabilities you don't need. Use seccomp to block dangerous system calls. Use minimal, regularly scanned, up-to-date images so there's less inside to exploit. This is the whole idea behind hardened base images like BellSoft Hardened Images, which run non-root and strip out package managers and other non-essentials on top of the small Alpaquita Linux base. Use AppArmor or SELinux to fence in what the process can touch. Avoid mounting sensitive host paths, especially the Docker socket! Containers shrink the blast radius, but they never zero it out. Treat every container as an application process that has some isolation and is absolutely not impenetrable, and you'll make far better decisions about what to lock down. The short version A Linux container is not a tiny Linux OS. It's an isolated process on a shared kernel, fenced in by namespaces and capped by cgroups. Get that one idea straight and a whole category of confusing incidents starts to make sense, because performance and security problems tend to happen exactly at the seam between the container, the runtime, and the host. Two things to do with this today: set explicit CPU, memory, and PID limits and load-test with them on, and start from a minimal, non-root, regularly scanned image instead of a general-purpose one. Everything else is easier once those two are in place. If you want to go deeper, especially on the JVM side, these two are worth your time: JVM in Linux containers: surviving the isolation Docker image security best practices for production - [BellSoft Announces Hardened Builder for Paketo Buildpacks™, Bringing Zero-CVE Container Images to Buildpacks® Users](https://bell-sw.com/news/bellsoft-announces-hardened-builder-for-paketo-buildpacks-bringing-zero-cve-container-images-to-buildpacks-users/): BellSoft Announces Hardened Builder for Paketo Buildpacks™, Bringing Zero-CVE Container Images to Buildpacks® UsersSan Jose, California (July 21, 2026) BellSoft, a leading OpenJDK vendor, announces the general availability of a new hardened builder image for Paketo Buildpacks™. Built entirely on BellSoft Hardened Images, the builder gives Paketo Buildpacks™, users a direct path to improved security and compliance posture, including continuous vulnerability management under SLA, signed images, and Software Bill of Materials, all inherited automatically with no changes to existing developer workflows. Paketo Buildpacks™ is an open-source project, backed by the Cloud Native Computing Foundation, that transforms application source code into production-ready container images automatically, without a Dockerfile. A buildpack inspects application code, determines what it needs, downloads dependencies, compiles where necessary and produces a runnable OCI image, all in a single command. At the center of the process is a builder, a curated container image that packages the buildpacks, a build environment (the “build stack”), and a runtime environment (the “run stack”) together. The builder is the security foundation of every image produced: the OS packages, libraries and runtime binaries it contains become the base of every application container that uses it. BellSoft’s contribution is a hardened builder that replaces both stacks with Hardened Images, based on Alpaquita, BellSoft's own secure and lightweight OS, so every container produced automatically inherits BellSoft’s full security and compliance posture. Buildpack tools vs. Dockerfiles: why enterprises are switching Dockerfiles are written by developers, one per service, updated manually, and are prone to drift. According to a recent BellSoft survey, over 60% of developers are unaware that a poorly written Dockerfile can itself become a security vulnerability. At enterprise scale, with hundreds of services and dozens of teams, Dockerfile sprawl becomes a compliance and maintenance liability. Base image updates must be propagated manually across every repository. Security patches are only as fast as the slowest team. Paketo Buildpacks™ concentrate container build expertise in a single, reusable layer maintained by platform engineering. Developers push code, and the buildpack tooling auto-detects the language, resolves dependencies, and produces a minimal, reproducible OCI image with a built-in Software Bill of Materials. No Dockerfile is required. When a base image needs updating, buildpack rebasing patches the OS layer across every service on the next build, without touching application code. Among BellSoft’s user base, buildpack tool adoption more than doubled year-over-year from 2024 to 2025. With BellSoft’s hardened builder, that advantage compounds. When a vulnerability is patched in BellSoft Hardened Images, every application built on the builder picks up the fix on the next build, across every service and every team, simultaneously. Hardened images should be the default, not the upgrade The threat landscape has changed. Zero-trust security models, software supply chain attacks, and tightening regulatory requirements, from FedRAMP and DISA STIG to the EU Cyber Resilience Act (CRA) and DORA, have made hardened container bases a practical necessity for any organization running production workloads. The question is no longer whether to harden, but how to do it without adding friction for development teams. BellSoft Hardened Images are built on Alpaquita, BellSoft's secure, lightweight OS, and ship with a significantly reduced package footprint, non-mutable component set, running as non-root, and other security configurations enabled by default. They are maintained by BellSoft’s security team with a commitment to a zero-CVE window: when a vulnerability is disclosed, a patched image is published typically within 24 hours. “Vulnerability management is a business problem, not an engineering one,” said Alex Belokrylov, CEO of BellSoft. “It deserves a business answer, not the silent accumulation of toil on already-stretched internal teams. Scanner fatigue is real, and so is the cost of ignoring it. Rather than tracking CVE feeds, triaging which vulnerabilities affect which base images, and coordinating patches across teams, security and platform engineering teams can rely on BellSoft to maintain a clean image baseline. Each published image comes with a full Software Bill of Materials and a verifiable provenance record, making compliance audits straightforward and transparent, and providing the documented evidence that regulators and enterprise procurement teams increasingly demand.” Availability BellSoft’s hardened builder for Paketo Buildpacks™ is available now on Docker Hub for six language runtimes: Java (Liberica JDK), native images with Liberica NIK (GraalVM-based tool), Node.js, Python, Go, and Ruby. The builder supports both x86-64 and ARM64 architectures, and is available in two C standard library variants—musl and glibc—to match existing infrastructure requirements. Java workloads on Liberica JDK additionally benefit from a 30% reduction in RAM and disk usage compared to standard OpenJDK images. Teams already using Paketo Buildpacks™ can switch to the BellSoft hardened builder with a single configuration change. No application code modifications are required. Buildpacks, the Buildpacks.io logo, and Paketo Buildpacks are trademarks of The Linux Foundation. Please see buildpacks.io and paketo.io for more information. - [BellSoft Releases Hardened Builder for Paketo Buildpacks](https://bell-sw.com/blog/bellsoft-releases-hardened-builder-for-paketo-buildpacks/): We are happy to announce the release of a new hardened builder for Paketo Buildpacks. The hardened builder is based on BellSoft Hardened Images bringing teams improved security and compliance posture through low-to-zero CVEs design, continuous patching, SBOMs, digital signatures, and SLA. The hardened builder is available for Java, GraalVM Native Image, Python, Go, Ruby, and Node.js. This article describes how teams will benefit from a new hardened builder and how to get started with it. Table of Contents The Security Cost of Inconsistent Dockerfile Practices BellSoft’s Hardened Builder: A Standardized Approach to More Secure Application Image Builds Supported Languages and Availability Java GraalVM Native Image Python Go Node.js Ruby How to get Started with BellSoft’s Hardened Builder for Paketo Buildpacks Conclusion: A Hardened Default for Application Container Builds The Security Cost of Inconsistent Dockerfile Practices A good Dockerfile can produce a small, maintainable, secure image. But the trouble starts when every team writes its own Dockerfile. In this case, security depends on developers applying the same practices correctly across every service. That rarely holds. Small differences add up. One service uses an outdated base image; another ships build tools and unused OS packages; a third runs as root. Our survey found that more than 60% of developers do not realize a poorly written Dockerfile can be a security vulnerability. That is not a failure of effort. Application developers focus on the application, and container hardening is not their main job. A badly written Dockerfile is one problem. Another is maintaining hundreds of Dockerfiles. When a base image vulnerability appears, security or platform teams must find affected services, get each team to update, rebuild, test, and redeploy. Base image sprawl makes the CVE picture even messier and compliance harder to prove. Buildpacks replace those scattered decisions with a shared build process. BellSoft’s hardened builder adds a securely maintained, continuously patched OS and runtime baseline. BellSoft’s Hardened Builder: A Standardized Approach to More Secure Application Image Builds BellSoft has already been providing its own open source builder for Paketo buildpacks with Java support based on minimalistic Alpaquita Linux with musl and glibc variants. The hardened builder builds on the momentum by extending the language support beyond Java to Python, Go, Ruby, and Node.js and moving the run image to BellSoft Hardened Images. As a result, BellSoft’s hardened builder provides: Standardized builds across languages; Minimal, reproducible OCI images without the need to Dockefiles; Hardened image baseline with low-to-zero CVEs at release time; Continuous patching; Provenance data with SBOMs and digital signatures. This way, teams will benefit both from standardization offered by buildpacks in general and the hardened base for their applications. Instead of requiring every service team to define and maintain its own Dockerfile, platform teams can provide a common build path across supported languages. The buildpacks handle the repeatable parts of image creation, while BellSoft Hardened Images provide a hardened, continuously patched foundation for the resulting runtime image. This gives organizations a more consistent starting point for container security. Teams can reduce base image sprawl and apply common build and runtime practices. In addition, rather than coordinating separate Dockerfile changes across many repositories, teams can follow a common rebuild or rebase workflow when updates are available. Supported Languages and Availability BellSoft’s hardened builder is available on Docker Hub for x86-64 and ARM64 architectures for the following languages and ecosystems: Java GraalVM Native Image Python Node.js Go Ruby The builder is available in two C library variants musl and glibc. Each language uses the same general workflow: select the relevant buildpack, build the application image, test it, push it to your registry, and deploy it through your existing platform. Java BellSoft’s hardened builder for Java is based on Liberica JDK Lite optimized for the cloud, which helps to reduce the final image size for Java applications by at least 30% compared to standard OpenJDK images. The builder supports all LTS Java versions, 8, 11, 17, 25, and the latest JDK version. To build a Java container image with musl-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:musl --path To build a Java container image with glibc-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:glibc --path See the GitHub project page for the list of available configuration options. Also, to learn how to use and configure buildpacks with Spring Boot, see this article. GraalVM Native Image BellSoft’s hardened builder is based on Liberica Native Image Kit, a downstream version of GraalVM CE with Liberica JDK as the underlying JVM. Liberica NIK Versions 22 and 23 are supported. To build a native image executable out of a Java application with musl-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:musl --path --env BP_NATIVE_IMAGE=true To build a native image executable out of a Java application with glibc-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:glibc --path --env BP_NATIVE_IMAGE=true For more information about building native images and available options, see Build an App as a GraalVM Native Image Application. Python BellSoft’s hardened builder for Python supports Python version 3.14. To build a Python container image with musl-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:musl --path To build a Python container image with glibc-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:glibc --path For more information about building Python applications and available options, see How to Build Python Apps with Paketo Buildpacks. Go BellSoft’s hardened builder for Go supports Go version 1.26. To build a Go container image with musl-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:musl --path To build a Go container image with glibc-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:glibc --path For more information about building Go applications and available options, see How to Build Go Apps with Paketo Buildpacks. Node.js BellSoft’s hardened builder for Node.js supports Node.js version 24. To build a Node.js container image with musl-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:musl --path To build a Node.js container image with glibc-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:glibc --path For more information about building Node.js applications and available options, see How to Build Node.js Apps with Paketo Buildpacks. Ruby To build a Ruby container image with musl-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:musl --path To build a Ruby container image with glibc-based Alpaquita, run: pack build demo-app --builder bellsoft/buildpacks.builder:glibc --path For more information about building Ruby applications and available options, see How to Build Ruby Apps with Paketo Buildpacks. How to get Started with BellSoft’s Hardened Builder for Paketo Buildpacks The new hardened builder is used by default, no need to choose it explicitly. However, depending on where you are at using buildpacks — already using BellSoft’s builder, using buildpacks with other builders, or using Dockerfiles — the getting started guide will be slightly different. Already Using BellSoft’s Builder If you already use BellSoft’s builder, you need to rebuild the application image from source with the current builder and pull policy set to always. For instance, here’s how to do that with the pack CLI: pack build --builder bellsoft/buildpacks.builder: --pull-policy always The --pull-policy always option ensures pack checks the registry instead of using the builder image cached on the runner. Therefore, this pulls the latest builder and its current buildpacks before rebuilding the image. If you use buildpacks via Maven/Gradle plugin, you can specify the pull policy there. Note that you need to regularly update the images so that the fresh base with fixes and patches is used. You can use the command above or use rebase. Switching to BellSoft’s Builder from Another Builder If you already use buildpacks and want to switch to BellSoft’s hardened builder, you need to specify it with the --builder option. At the same time, you need to choose whether you want to use a musl- or glibc-based variant. The container image built with the musl-based builder will be slightly smaller than the one built with glibc. We recommend trying both of them and selecting the variant that performs best for your workload and fits your dependencies. Here’s the command for building an image with a musl-based BellSoft builder: pack build --builder bellsoft/buildpacks.builder:musl Here’s the command for building an image with a glibc-based BellSoft builder: pack build --builder bellsoft/buildpacks.builder:glibc For Java applications, if you use buildpacks with Maven or Gradle plugins, you can specify the builder in the plugin configuration. Maven: org.springframework.boot spring-boot-maven-plugin bellsoft/buildpacks.builder:musl Gradle: bootBuildImage { builder = "bellsoft/buildpacks.builder:musl" } Migrating to Buildpacks from Dockerfiles If your team has no prior experience with buildpacks, don’t migrate all workloads at once. Start with one non-critical service. Regardless of the language, the command for building a container image is the same. The builder detects the language automatically. Build the image from source with the BellSoft’s builder either using the pack CLI: pack build --path --builder bellsoft/buildpacks.builder: Where is either musl or glibc. Alternatively, use the Maven/Gradle plugin if you build Java applications. For example, here’s how to use buildpacks with Spring Boot. Maven: org.springframework.boot spring-boot-maven-plugin paketo-maven bellsoft/buildpacks.builder:musl Build the image with: mvn spring-boot:build-image Gradle: plugins { id 'java' id 'org.springframework.boot' version '3.0.6' id 'io.spring.dependency-management' version '1.1.0' } dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' } bootBuildImage { imageName = "paketo-gradle" builder = "bellsoft/buildpacks.builder:musl" } Build the image with: gradle bootBuildImage After building the container image, run the usual functional and integration tests, and compare the result with the current Dockerfile-based image. Check that the application starts correctly, exposes the expected ports, reads its configuration, and works with the existing deployment pipeline. After successful validation, you can start migrating other services using the same process. When updated hardened base images become available, rebuild or rebase the application image and redeploy it through the normal release process. Note that the update requires an explicit action from the team, but the workflow is consistent across supported applications. Conclusion: A Hardened Default for Application Container Builds Container security should not rely on every team maintaining a flawless Dockerfile. BellSoft’s hardened builder for Paketo buildpacks gives Java, Python, Go, Ruby, and Node.js teams a standard way to build application images on a hardened, continuously patched foundation. Try BellSoft’s hardened builder for your application image and start from a more secure baseline. If you have any questions about BellSoft's builder or other products or would like to request support, contact us, and our engineers will be happy to help you. Contact us - [Liberica JDK 8u502, 11.0.32, 17.0.20, 21.0.12, 25.0.4, and 26.0.2 builds are generally available](https://bell-sw.com/blog/liberica-jdk-8u502-11-0-32-17-0-20-21-0-12-25-0-4-and-26-0-2-builds-are-generally-available/): We are happy to announce the general availability of a Critical Patch Update (CPU) of Liberica JDK versions 6u511, 7u511, 8u501, 11.0.31.0.1, 17.0.19.0.1, 21.0.11.0.1, 25.0.3.0.1. CPU releases are stabilized builds that include patches for Common Vulnerabilities and Exposures (CVE) described in the relevant CVE entries in BellSoft’s Security Advisory. BellSoft is one of only three companies including Oracle that release CPU builds aimed at eliminating known security issues without disrupting the production environment. In addition, we release PSU versions 8u502, 11.0.32, 17.0.20, 21.0.12, 25.0.4, and 26.0.2 with non-critical fixes and general improvements. The release contains 1252 fixes and backports overall. BellSoft participated in eliminating 35 issues in all releases. How to keep your runtime secure BellSoft recommends updating Liberica JDK with each Critical Patch Update (CPU) to ensure the stable work and secure performance of the runtime. CPUs are scheduled for release in January, April, June, and October every year. Liberica JDK updates and patches are available at no cost. Download Liberica JDK The summary of fixes 18 security issues (CVEs) fixed. 89 total security fixes (+ 22 additional non-security fixes) in CPU release: in Liberica 6u511: 13 security fixes + 11 additional fixes; in Liberica 7u511: 13 security fixes + 11 additional fixes; in Liberica 8u501: 13 security fixes; in Liberica 11.0.31.0.1: 14 security fixes; in Liberica 17.0.19.0.1: 12 security fixes; in Liberica 21.0.11.0.1: 12 security fixes; in Liberica 25.0.3.0.1: 12 security fixes. In addition, PSU releases include a total of 1141 fixes and backports: in Liberica 8u502: 13 security fixes (+ 5 in FX) + 38 additional fixes (+ 10 in FX); in Liberica 11.0.32: 14 security fixes (+ 5 in FX) + 54 additional fixes (+ 9 in FX); in Liberica 17.0.20: 12 security fixes (+ 5 in FX) + 203 additional fixes (+ 14 in FX); in Liberica 21.0.12: 12 security fixes (+ 6 in FX) + 265 additional fixes (+ 13 in FX). in Liberica 25.0.4: 12 security fixes (+ 6 in FX) + 233 additional fixes (+ 11 in FX); in Liberica 26.0.2: 12 security fixes (+ 6 in FX) + 166 additional fixes (+ 17 in FX). Download Liberica JDK List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2026-47057 7.5 core-libs javax.script network low none none unchanged none none high CVE-2026-41254 7.5 client-libs 2d network low none none unchanged none none high CVE-2026-47063 7.5 security-libs java.security network low none none unchanged none high none CVE-2026-47058 7.4 core-libs javax.script network high none none unchanged high high none CVE-2026-60147 6.5 security-libs java.security network low none none unchanged low low none CVE-2026-46968 5.9 security-libs javax.net.ssl network high none none unchanged none high none CVE-2026-47027 5.3 security-libs java.security network low none none unchanged none none low CVE-2026-47021 5.3 client-libs 2d network low none none unchanged none none low CVE-2026-46917 5.3 security-libs javax.net.ssl network low none none unchanged none none low CVE-2026-47059 5.9 client-libs 2d network high none none unchanged none none low CVE-2026-47010 3.7 client-libs javax.imageio network high none none unchanged none low none CVE-2026-47013 5.3 javafx graphics network low none none unchanged none none low CVE-2026-47030 3.1 javafx web network high none required unchanged none low none CVE-2026-47034 3.1 javafx media network high none required unchanged none low none CVE-2026-47035 3.1 javafx media network high none required unchanged none low none CVE-2026-60164 3.1 javafx web network high none required unchanged low none none CVE-2026-60165 3.1 javafx media network high none required unchanged none low none CVE-2026-60166 3.1 javafx media network high none required unchanged low none none Summary of fixes in Liberica JDK CVEs fixed in Liberica per version: CVE ID 8 11 17 21 25 26 CVE-2026-47057 𑇐 𑇐 CVE-2026-41254 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47063 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47058 𑇐 𑇐 CVE-2026-60147 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-46968 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47027 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47021 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-46917 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47059 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47010 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47013 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47030 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47034 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-47035 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-60164 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-60165 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 CVE-2026-60166 𑇐 𑇐 𑇐 𑇐 𑇐 𑇐 Supported platforms Liberica JDK is tested and proven to work on a large number of platforms. Liberica JDK can be run in virtual and cloud environments. The following hypervisors are supported: Docker KVM Microsoft Hyper-V (gen 1 and gen 2) VirtualBox VMware vSphere Hypervisor Solaris Containers & Solaris LDOMs Liberica JDK supports all major cloud providers, including but not limited to: Amazon AWS Digital Ocean Google Cloud Microsoft Azure OVH Packet Scaleway VMware Tanzu Enjoy the most stable runtime! The CPU release cycle enables the OpenJDK community to introduce security patches and bug fixes to Java as soon as possible, thus minimizing the risk of attacks on your applications. Download the new Liberica JDK builds now! Click on the button below to head over to Liberica Download Center. Download Liberica JDK - [Liberica JDK and Liberica NIK Are Moving Toward Monthly Security Updates](https://bell-sw.com/blog/liberica-jdk-and-liberica-nik-are-moving-toward-monthly-security-updates/): The pace of Java security updates is set to increase. The industry is moving from the long-standing quarterly rhythm of January, April, July, and October toward more frequent, monthly security releases, and BellSoft is moving with it. As an active OpenJDK contributor, we intend to deliver Liberica JDK and Liberica Native Image Kit security updates on this faster cadence, beginning with an additional release in August alongside the standard July and October updates. Why it matters: vulnerability discovery has entered a new era. AI-assisted analysis helps researchers, and attackers too, find and weaponize flaws in days rather than months. Every week between a disclosure and a patched runtime is a week of exposure, and a monthly cadence shrinks that window for every Liberica user. BellSoft typically publishes Liberica releases within 48 hours of the corresponding upstream updates, and we intend to keep that pace as the cadence accelerates. If you ship applications with a bundled Liberica runtime, this is a good time to revisit the full JDK versioning scheme defined in JEP 322, and to make sure your build and deployment tooling correctly handles the emergency patch-release counter used for out-of-cycle security releases. Watch this blog or sign up for our product alerts for the August release date and further details. - [Liberica Native Image Kit 23.0.13, 23.1.12, and 25.0.4 builds are released](https://bell-sw.com/blog/liberica-native-image-kit-23-0-13-23-1-12-and-25-0-4-builds-are-released/): We are happy to announce the general availability of Liberica Native Image Kit (NIK) versions 23.0.13 for JDK 17, 23.1.12 for JDK 21, and 25.0.4 for JDK 25 as part of Critical Patch Update (CPU) release cycle. The builds contain several security and bug fixes. All Liberica NIK builds contain the latest version of Liberica JDK with fixes and eliminated security issues. Notable improvements List of security issues fixed CVE ID cvss score component module Attack vector (network/local) Complexity (low/high) Privileges (none/low) User interaction (none/required) Scope (changed/unchanged) Confidentiality (low/none/high) Integrity (low/none/high) Availability (low/none/high) CVE-2026-47057 7.5 core-libs javax.script network low none none unchanged none none high CVE-2026-41254 7.5 client-libs 2d network low none none unchanged none none high CVE-2026-47063 7.5 security-libs java.security network low none none unchanged none high none CVE-2026-47058 7.4 core-libs javax.script network high none none unchanged high high none CVE-2026-60147 6.5 security-libs java.security network low none none unchanged low low none CVE-2026-46968 5.9 security-libs javax.net.ssl network high none none unchanged none high none CVE-2026-47027 5.3 security-libs java.security network low none none unchanged none none low CVE-2026-47021 5.3 client-libs 2d network low none none unchanged none none low CVE-2026-46917 5.3 security-libs javax.net.ssl network low none none unchanged none none low CVE-2026-47059 5.9 client-libs 2d network high none none unchanged none none low CVE-2026-47010 3.7 client-libs javax.imageio network high none none unchanged none low none CVE-2026-47013 5.3 javafx graphics network low none none unchanged none none low CVE-2026-47030 3.1 javafx web network high none required unchanged none low none CVE-2026-47034 3.1 javafx media network high none required unchanged none low none CVE-2026-47035 3.1 javafx media network high none required unchanged none low none CVE-2026-60164 3.1 javafx web network high none required unchanged low none none CVE-2026-60165 3.1 javafx media network high none required unchanged none low none CVE-2026-60166 3.1 javafx media network high none required unchanged low none none Download the new builds now! BellSoft strives to provide Java developers with a full stack of secure and affordable technologies suitable for creating a wide range of applications. And thanks to the CPU release cycle, your applications will be secure at all times. Download the latest version of Liberica NIK now! Download Liberica NIK