Performance tuning
Last updated
© Leapwise
Last updated
The code we are writing should be clean and we try to write it as production ready in the first attempt. It is not only that it will help us to have a better understanding of it, but it will also make the application more effective. The following paragraphs contain a description of guidelines that will help us to reach both.
Procedural code is hard to read for humans and PCs. Local variables that are primary building blocks of procedural code are stored on a Stack and kept there until method processing is finished. This already suggests that procedural code will consume more memory. Furthermore, the Just-in-Time compiler will have a hard time compiling such code and improving its performance - code might be even untouched by JIT.
When possible, use int instead of Integer, double instead of Double, … Primitives are stored on Stack and objects are stored on Heap - we want to use Stack more often because it is faster.
When regular expressions need to be used in computation-intensive code, it is advised to cache the pattern reference rather than compile it every time. Additionally, if any data is frequently accessed from a disk or database, it should be cached.
String concatenation is expensive, although it is often needed. If concatenation is needed, a combination of strings and a '+' sign should be avoided. Instead, StringBuilder should be used to prevent the creation of multiple string objects when concatenation is performed.
Java provides a wide range of data structures in its Collections Framework, such as ArrayList, LinkedList, HashMap, TreeSet, …
Each of those has a usage but it is up to the developer to understand their performance and choose the one that best suits his needs.
Java’s garbage collector helps manage memory by automatically reclaiming the memory by deleting the objects that are no longer in use. However, garbage collection can also be a source of performance overhead and it can be further optimized. Consider the following settings when doing the optimization:
-XX:+UseG1GC: : Use the Garbage First (G1) Collector.
-XX:ParallelGCThreads: Sets the number of threads used during parallel phases of the garbage collectors. The default value varies with the platform on which the JVM is running.
-XX:ConcGCThreads: Number of threads concurrent garbage collectors will use. The default value varies with the platform on which the JVM is running.
-XX:MaxGCPauseMillis: Sets a target for the maximum GC pause time. This is a soft goal, and the JVM will make its best effort to achieve it.
-XX:+ParallelRefProcEnabled: Recommend setting this value to enable parallel reference processing. By default, this option is disabled.
-XX:+ResizePLAB: Enable or disable resizing of Promotion Local Allocation Buffers (PLABs) by using a + or - symbol before the parameter name.
-XX:InitiatingHeapOccupancyPercent: Sets the Java heap occupancy threshold that triggers a marking cycle. The default occupancy is 45 percent of the entire Java heap.
-XX:G1ReservePercent: Sets the percentage of reserve memory to keep free to reduce the risk of to-space overflows. The default is 10 percent. When you increase or decrease the percentage, make sure to adjust the total Java heap by the same amount.
-XX:MinHeapFreeRatio: Sets the minimum percentage of heap free after GC to avoid expansion.
Understanding our application's memory requirements is crucial for optimal performance. It's essential to have a clear grasp of the data volume the application handles at peak usage and identify potential bottlenecks. Additionally, awareness of the broader environment, such as the number of CPUs and available RAM, is vital for successful deployment.
Ideally, developers should proactively communicate the application's memory needs along with the environment requirements. To achieve this, the memory requirements for the application must be explicitly determined. By default, if not specified otherwise, the Java Virtual Machine (JVM) allocates 25% of the environment's memory as the maximum limit for the application.
That is why we need to specify the minimum and maximum heap size memory used by the application. We can say that defining explicit heap memory is mandatory for each application and it can be done with the usage of -Xms
(sets the initial and minimum heap size) and -Xmx
(sets the maximum heap size) parameters:
Oracle recommends that -Xms
and -Xmx
be set to the same value. This gives us a controlled environment where we get an appropriate heap size right from the start.
Allocating too little memory can have significant performance impacts. It can lead to garbage collector issues, system freeze, and severe system performance issues.
Allocating too much memory can lead to lengthy garbage collection pauses and lengthy memory defragmentation (also known as compaction). That in turn may lead to system failures. Make sure that the maximum heap size does not exceed 8192 megabytes.
The provided guidelines are intended to highlight the significance of minor code enhancements in influencing code performance. As applications become more complex, the likelihood of encountering performance issues in various areas increases. In such instances, monitoring the application may aid in addressing the identified issues.
if-else
if-else statements can affect Java performance because they require JVM to evaluate each condition sequentially. To optimize it, if-else conditions should be simple, and short-circuit evaluation should be used when possible.
switch
switch statements are not encouraged by clean code, but long chains of if-else statements should be replaced with a switch. It allows JVM to jump directly to the relevant block of code.
Loops
Loops are often the primary source of performance bottlenecks. for-each loop should be used instead of for-loop when possible. Method calls and calculations within the loop body should be minimal and (ideally) no object should be created.