Introduction
Optimizing the performance of Java applications is not a one-time task, but a continuous process that requires an understanding of both the Java Virtual Machine (JVM) and the specifics of the code itself. In today's world, where response speed and throughput directly impact user experience and business metrics, the ability to identify and eliminate bottlenecks becomes a critically important skill. In this article, we will look at key optimization approaches: from configuring the garbage collector and memory management to the effective use of multithreading and profiling. Each section will be supported by practical examples in Java.
1. Profiling and Identifying Bottlenecks
Before optimizing anything, it is necessary to precisely determine where time or memory is being spent. Blind optimization may not only fail to produce results but can also degrade performance. Profilers exist for this purpose.
1.1. Profiling Tools
The most popular tools for Java:
- VisualVM — a free tool built into the JDK. It allows monitoring CPU, memory, threads, and performing heap dumps.
- JProfiler and YourKit — commercial solutions with advanced analysis capabilities.
- Async Profiler — a lightweight profiler for CPU and memory, well-suited for production.
1.2. Example: Finding "Hot" Methods
Suppose we have a method that is called frequently and runs slowly. Using a profiler, we see that 70% of the time is spent on string concatenation in a loop:
public class StringConcatenation { public static String buildString(int n) { String result = ""; for (int i = 0; i < n; i++) { // Bad: a new String object is created each time result += "item" + i + ","; } return result; }
public static void main(String[] args) { long start = System.nanoTime(); String s = buildString(100_000); long end = System.nanoTime(); System.out.println("Time: " + (end - start) / 1_000_000 + " ms"); }}After profiling, we understand that we need to replace the concatenation with StringBuilder:
public static String buildStringOptimized(int n) { StringBuilder sb = new StringBuilder(n * 10); // Pre-allocation for (int i = 0; i < n; i++) { sb.append("item").append(i).append(","); } return sb.toString();}This is a very simple example, but it clearly demonstrates the importance of profiling: without it, we wouldn't know what exactly needs to be fixed.
2. JVM and Garbage Collector Tuning
The JVM is the runtime environment, and its configuration can yield a huge performance boost. The key parameters relate to memory and garbage collection (GC).
2.1. Basic Memory Parameters
-Xms— initial heap size.-Xmx— maximum heap size.-Xss— stack size for each thread.
It is recommended to set -Xms and -Xmx to the same value to avoid the overhead of heap expansion during runtime.
2.2. Choosing a Garbage Collector
Modern Java versions offer several GCs:
- G1GC (Garbage First) — the default since Java 9. A good balance between throughput and latency.
- ZGC — for applications with very low latency (pauses under 10 ms).
- Shenandoah — similar to ZGC, but with a different algorithm.
- Parallel GC — for applications where maximum throughput is important, rather than response time.
Example of launching with G1GC and logging the work