How to Improve the Performance of Legacy Java Code

Transcript:

Your application is running on Java 8 or 11. Well, the code is set and true, but the performance requirements are new. Do I speak in rhymes now? Well, anyway, you have to increase the performance, but there is a catch - you are not allowed to upgrade to a newer Java version, or the migration is planned for sometime in the future. But you have to reach KPIs now. So what do you do? Stick around as I show you the techniques that will help to speed up your Java application, reduce garbage collection pauses and memory footprint - all of that without upgrading the Java version.

Why legacy Java code underperforms? According to the survey our team recently conducted, half of the respondents are concerned about the performance of their legacy Java code. Almost a quarter allocate an additional budget to meet the new performance requirements, and yet migration to a newer Java version is not a business priority for 21 percent of the respondents. So the problem is that the management doesn't want to allocate resources or a budget to upgrade to a newer Java version - it is a lengthy, complicated process after all. But SLAs have to be met, and they are broken.

But why can't we let sleeping dogs lie? Why can't we leave the application alone, stay in legacy software, and be satisfied with the performance as it is now? After all, it has always been like that. What changed?

Well, many modern applications are deployed to the cloud, and the performance requirements in the cloud are different. Plus, as the user base grows, it leads to unpredictable traffic surges and growth - and the application can't handle the incoming traffic.

Why legacy Java struggles with modern workloads? First, the application takes long GC pauses and can't manage the incoming traffic due to older Java garbage collectors - and so, user dissatisfaction grows. Secondly, older Java versions have bigger CPU overhead, which leads to resource underutilization and increased costs. Thirdly, the memory footprint of legacy Java versions is bigger. As a result, you have to allocate more resources to cloud instances, which leads to increased cloud bills.

So what can we do about that?

Optimization strategies. There are three possible approaches to increasing the performance of Java workloads without upgrading the Java version: You can refactor the application code and adjust JVM configuration. You can scale the cloud infrastructure. You can use a fused JDK. Let's look at all three approaches.

Changes to the application code and JVM configuration. Tune the garbage collector. Garbage collection implementation can affect the application performance. There are several collectors to choose from, plus you can adjust the settings of each of them to reach the required result. GC tuning should be based on the workload. Parallel GC is best for CPU-bound batch processing. For Java 8, you can use CMS GC to decrease inner latency, as it is best suited for lower latency applications. You can also try G1 GC, which became the default collector in newer Java versions. For Java 11, try out G1 GC, as CMS GC is deprecated. You can also experiment with ZGC, which is a low-latency garbage collector, but keep in mind that it is not ready for production in this version. You can analyze GC behavior using Java profilers such as Java Flight Recorder.

Adjust JVM memory settings. You can reduce the memory footprint of your application by adjusting the JVM memory settings. In some cases, setting the minimum and maximum Java heap size is enough to reduce the Java memory footprint. To limit the total RAM consumption, you can use the Max RAM flags - they are very useful for containers. Of course, there are many more memory flags. I have collected the most common of them with the explanation in a dedicated article. The link is in the description.

Improve threading and concurrency. Optimizing concurrency can help the application to handle increased workloads and user interaction in a more efficient way. Some recommendations are to identify synchronization bottlenecks, use thread pools instead of raw threads, and monitor the concurrent code with jstack, Java Flight Recorder, or async profiler.

Optimize database calls. Inefficient database queries can hugely impact the application performance or even lead to memory leaks. First of all, you should analyze the queries using a specialized tool for that or a profiler that supports SQL assessment. For instance, you can use Digma AI in IntelliJ IDEA Ultimate. You can also use EXPLAIN ANALYZE profiling tools if you use MySQL. Some general recommendations: use the JPA second-level cache before considering external caches like Redis, avoid N+1 query problems, and batch fetch related entities properly instead of using lazy loading.

Performance optimizations with the right cloud infrastructure. Changing the code is problematic and complicated, so another approach to boosting the performance of the application is to scale the cloud infrastructure. There are two approaches to scaling: vertical scaling and horizontal scaling.

Vertical scaling (or scaling up) is about adding more CPU, memory, or disk to a single machine - so, improving the hardware. Its benefits are simple setup, no code changes are required, and it can improve the single instance performance quickly. But it can be expensive for large-scale needs, and it doesn't improve availability of the application.

Horizontal scaling (or scaling out) is about adding more instances to distribute the load. Horizontal scaling works well for stateless Java applications. It is cost-effective with auto-scaling in cloud environments, and it improves fault tolerance. But it might require code changes, such as configuring the session management.

Optimize memory usage in the cloud. Instead of fine-tuning the JVM memory options, you can change the base image for your containers and use lightweight Alpine Linux - this will immediately reduce the disk space consumed by the application. In Java 11, you can use jlink to cut out a custom runtime and reduce the container image size even more.

Switch to a fused JDK. Newer Java versions include lots of new features and JVM improvements which are worth the migration. But if you need a performance boost right now, and upgrading the Java version is currently not an option, you can use a fused JDK. It can increase the performance of your application without code changes.

So what is a fused JDK? I'm talking about Liberica JDK Performance Edition. It couples JDK 8 or 11 with JVM 17. As a result, it brings the improvements to garbage collection, intrinsics, and compiler to the older JDK versions. For instance, you get the improved G1 GC, production-ready ZGC, and Nano GC for your JDK 8 to 11-based workloads without upgrading the Java version. Essentially, your dependencies are preserved - you don't need to change the library versions or the application code. In some cases, you might need to change some JVM options which were deprecated or renamed in later JVM versions.

So you stay on JDK 8 or 11, and the performance of your application gets immediately improved by 10 or even 15 percent, even at the default settings. And of course, you can switch to a modern low-latency collector such as ZGC, and it will do wonders to latency. Liberica JDK is 100% open source. It means that you can easily migrate to or from it without vendor lock-in.

Conclusion. So what options do we have? You can change the application code, or you can scale the cloud infrastructure. But it will only help so much, because older JDK versions have performance limits that you can't surpass. Plus, you need to change a lot - and that costs time and money. So the easiest solution is to switch to Liberica JDK Performance Edition.

Don't take my word for it - you can read more about Liberica JDK Performance Edition under the link I pasted in the description box. You can also request a demo version of Liberica JDK Performance Edition to test it with your code and see which performance improvements you can get. Don't forget to like this video, subscribe, and watch other related videos on our channel - such as the overview of Java garbage collectors or profiling Java applications. See you next time.

Summary

In this video, we explore how to improve the performance of Java 8 or 11 applications without upgrading to a newer Java version. Many companies face modern performance demands, especially in the cloud, but migration is often delayed due to cost and complexity. The video presents three approaches: optimizing application code and JVM settings, scaling cloud infrastructure, or using a fused JDK like Liberica JDK Performance Edition. This fused JDK pairs legacy Java with a modern JVM, enabling better garbage collection, lower latency, and improved efficiency without changing application code. Additional tips include tuning GC, memory, threading, and database queries. With these strategies, performance can improve by up to 15% even on legacy Java.

Videos
card image
Apr 2, 2026
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.

Videos
card image
Mar 26, 2026
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.

Further watching

Videos
card image
Apr 30, 2026
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/)

Videos
card image
Apr 22, 2026
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.

Videos
card image
Apr 8, 2026
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.