Java application performance has always been a mere buzzword until you face its real implications. It may vary depending on your interpretation and experience. In this blog, I am focusing specifically on Java heap and Java performance tuning.
You may be facing one of the issues listed below:
- Slow Application-
Your application may not be responding well because it's spending too much time in cleaning up the garbage rather than running the necessary processes.
- Consumes too much memory-
The memory usage of the application is related to the number and size of live objects that are in the Java virtual machine (JVM) at any given point of time. This can be either due to valid objects that are required to stay in Java heap memory, or because the programmer forgot to remove the reference to unwanted objects (typically known as 'memory leaks' in Java parlance).
Below are a few simple Java performance tuning tips which you can follow during your development process:
- JVM memory allocation
When the memory footprint hits the threshold, the JVM throws the java.lang.OutOfMemoryError exception. This exception mainly occurs due to possible reasons listed below:
- JavaHeap space is low to create new objects. This can be increased by
-Xmx (java.lang.OutOfMemoryError: Java heap space).java.lang.OutOfMemoryError: Java heap space
- Permanent generation can be low. This can be increased by-
XX:MaxPermSize=256m(java.lang.OutOfMemoryError: PermGen space)java.lang.OutOfMemoryError: PermGen space
- Swap space can run out because of a
The Java Native Interface (JNI) Heap runs low on memory, even though the JavaHeap and the PermGen have memory. This typically happens if you are making lots of heavy JNI calls, but the JavaHeap objects occupy little space. In that scenario, the garbage collector (GC) thread might not feel the urge to clean up JavaHeap memory, while the JNI Heap keeps on increasing till it goes out of memory.
It is recommended to increase the Java heap space only up to one-half of the total RAM available on the server. Increasing the Java heap space beyond that value can cause performance problems. For example, if your server has 16 GB of RAM available, then the maximum heap space you should use is 8 GB.
If you are using Tomcat, you can follow the steps below:
Open the catalina.bat file (TomcatInstallDirectory/bin/catalina.bat).
Set JAVA_OPTS=%JAVA_OPTS% -Xms1024m -Xmx1024m
[Where Xms is the initial (start) memory pool and Xmx is the maximum memory pool]
- JavaHeap space is low to create new objects. This can be increased by
- Programmatic concatenating of strings
StringBuilder can be used to concatenate strings programmatically. There are lots of different options to concatenate strings in Java. You can, for example, use a simple “+” or “+=”, the good old StringBuffer, or a StringBuilder.
- Use of primitives
where possible. Another quick and easy way to avoid any overhead and improve the performance of your application is to use primitive types instead of their wrapper classes. So, it’s better to use an “int” instead of an Integer, or a “double” instead of a Double. That allows your JVM to store the value in the stack instead of the heap to reduce memory consumption and overall handle it more efficiently.
- Avoiding BigInteger and BigDecimal
As we’re already talking about data types, we should also take a quick look at BigInteger and BigDecimal. Especially the latter one is popular because of its precision. But that comes at a price. BigInteger and BigDecimal require much more memory than a simple “long” or “double” and slow down all calculations dramatically.
- Checking current log levels
This recommendation should be obvious, but unfortunately, you can find lots of code that ignores it. Before you create a debug message, you should always check the current log level first. Otherwise, you might create a string with your log message that will be ignored afterward.
- Avoiding “+” to concatenate strings in in one statement
When you implemented your first application in Java, someone probably told you that you shouldn’t concatenate strings with “+”. And that’s correct if you’re concatenating strings in your application logic. Strings are immutable, and the result of each string concatenation is stored in a new string object. That requires additional memory and slows down your application, especially if you’re concatenating multiple strings within a loop.
- Using ArrayList instead of LinkedList
ArrayList internal node traversal from the start to the end of the collection is significantly faster than LinkedList traversal. Consequently, queries implemented in the class can be faster. Iterator traversal of all elements is faster for ArrayList compared to LinkedList. ArrayLists can generate far fewer objects for the GC to reclaim, compared to LinkedLists.
- Using custom conversion methods for converting between data types
To reduce the number of temporary objects, always use custom conversion methods, especially strings and streams.
- Don’t optimize before you know it’s necessary
This might be one of the most important performance tuning tips. You should follow common best practices and try to implement your use cases efficiently. But that doesn’t mean that you should replace any standard libraries or build complex optimizations before you proved that it’s necessary.
- Understanding the real cost of streams
Streams are a great addition to the Java language, letting you easily lift error-prone patterns from “for” loops into generic ones, resulting in more reusable blocks of code with consistent guarantees. But this convenience doesn’t come for free, there is a performance cost associated with using streams. Thankfully, it appears this cost isn’t too high.
- Being conscious of formatting and parsing costs
Always be conscious of the cost of parsing and formatting date objects. Unless you need the string right then, it’s much better to represent it as a UNIX timestamp.
- Use Apache Commons StringUtils.replace instead of string.replace
In general, the string.replace method works fine and is efficient, especially if you’re using Java. But if your application requires a lot of replace operations and you haven’t updated to the newest Java version, it still makes sense to check for faster and more efficient alternatives.
- Iteration over recursion
Modern functional programming languages like Scala encourage the use of recursion, as they offer means of optimizing tail-recursing algorithms back into iterative ones. You might be wasting a lot of stack frames for something that might have been implemented using only a few local variables. So, always prefer iteration over recursion.
- Using EnumSet or EnumMap instead of HashSet and HashMap
There are some cases where the number of possible keys in a map is known in advance – for instance when using a configuration map. If that number is relatively small, you should really consider using EnumSet or EnumMap, instead of regular HashSet or HashMap instead.
- Avoid repeated calls when collection size is known
Iterator.hasNext() and Enumerator.hasMoreElements() do not need to be repeatedly called when the size of the collection is known. Use collection.size() and a loop counter instead.
- Optimize your hashCode() and equals() methods
A good hashCode() method is essential because it will prevent further calls to the much more expensive equals() as it will produce more distinct hash buckets per set of instances.
- Use the “finally” block well
The “finally” block is a key tool for preventing resource leaks. When closing a file or otherwise recovering resources, place the code in a “finally” block to ensure that the resource is always recovered. But “finally” is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a “return”, “continue”, or “break”. Putting cleanup code in a “finally” block is always a good practice, even when no exceptions are anticipated.
- Use a profiler to find the bottleneck
After you followed the first recommendation and identified the parts of your application you need to improve, there are various tools available in the market that can help in diagnosing the performance bottleneck. These tools will be covered in the next part of this blog series.