Java Application Performance Optimization: From Theory to Practice
Java remains one of the most popular languages for building enterprise and high-load systems. However, even a well-written application can eventually start to slow down, consume too much memory, or throw OutOfMemoryError errors. In this article, we will break down key methods for optimizing Java application performance that will help you make your code faster and more efficient. You will learn how to configure the JVM, optimize memory management, avoid common mistakes, and use modern profiling tools.
1. Profiling and Finding Bottlenecks
Before optimizing anything, you need to understand what is actually slowing things down. Intuitive guesses are often wrong — the bottleneck might not be the method that seems slow. Therefore, the first step is profiling. Use tools such as VisualVM, JProfiler, or the built-in Java Flight Recorder (JFR).
Example of starting profiling via JFR from the command line:
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \ -jar my-application.jarAfter recording, open the file in JDK Mission Control and look at:
- CPU profile — which methods consume the most processor time.
- Memory — which objects are created most frequently and take up the most space.
- Threads — whether there are any locks or long waits.
Only after analyzing the data can you begin making fixes. Without profiling, you risk optimizing something that is not the problem.
2. JVM and Garbage Collector Tuning
Proper configuration of the Java Virtual Machine (JVM) can provide a significant performance boost. The main parameters worth adjusting are:
- Heap size — parameters
-Xms(initial size) and-Xmx(maximum size). Do not set a very large initial size if the application does not need that much memory. - Garbage Collector (GC) selection — modern applications often benefit from G1GC (default since Java 9) or ZGC (for large heaps and low latency).
Example of launching with optimized settings:
java -Xms2g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+ParallelRefProcEnabled \ -jar my-app.jarPay attention to the -XX:MaxGCPauseMillis flag. It sets the target pause time for the garbage collector. If your application is sensitive to latency, try setting it to 100-200 ms. However, too small a value can cause frequent collections and reduce throughput.
It is also useful to enable GC logging to see how the collector is performing:
-Xlog:gc*:file=gc.log:time,uptime,level,tags3. Code Optimization: Efficient Data Structures and Algorithms
Even the best JVM tuning will not help if the code is written suboptimally. Here are a few specific recommendations with examples:
3.1. Use the Right Collections
Do not use ArrayList if you need to frequently insert elements at the beginning of the list — that is O(n). It is better to use LinkedList or ArrayDeque. For key-based lookups, use HashMap or TreeMap, not lists.
Bad example:
List<String> names = new ArrayList<>();for (int i = 0; i < 100000; i++) { names.add(0, "Name" + i); // O(n) each insertion}Good example:
List<String> names = new LinkedList<>();for (int i = 0; i < 100000; i++) { name