11 Simple Java Performance Tuning Tips
Want to keep your programs running performantly? Here are some steps you can take to eliminate bottlenecks, tips for caching, and other performance tuning suggestions.
Join the DZone community and get the full member experience.
Join For Free
Most developers expect that performance optimization is a complicated topic that requires a lot of experience and knowledge. Okay, that’s not entirely wrong. Optimizing an application to get the best performance possible isn’t an easy task. But that doesn’t mean that you can’t do anything if you haven’t acquired that knowledge. There are several easy to follow recommendations and best practices which help you to create a well-performing application.
Most of these recommendations are Java-specific. But there are also several language-independent ones, which you can apply to all applications and programming languages. Let’s talk about some of these generic ones before we get to the Java-specific performance tuning tips.
1. Don't Optimize Before You Know It's Necessary
That 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.
In most cases, premature optimization takes up a lot of time and makes the code hard to read and maintain. And to make it even worse, these optimizations most often don’t provide any benefits because you’re spending a lot of time optimizing non-critical parts of your application.
So, how do you prove that you need to optimize something?
First of all, you need to define how fast your application code has to be, e.g., by specifying a maximum response time for all API calls or the number of records that you want to import within a specified time frame. After you’ve done that, you can measure which parts of your application are too slow and need to be improved. And when you’ve done that, you should take a look at the second tip.
2. Use a Profiler to Find the Real Bottleneck
After you followed the first recommendation and identified the parts of your application you need to improve, ask yourself where to start?
You can approach this question in two ways:
- You can take a look at your code and start with the part that looks suspicious or where you feel that it might create problems.
- Or you use a profiler and get detailed information about the behavior and performance of each part of your code.
I hope I don’t need to explain why you should always follow the second approach.
It should be obvious that the profiler-based method gives you a better understanding of the performance implications of your code and allows you to focus on the most critical parts. And if you ever used a profiler, you will remember a few situations in which you were surprised by which parts of your code created the performance issues. More than once my first guess would have led me in the wrong direction.
3. Create a Performance Test Suite for the Whole Application
This is another general tip that helps you avoid a lot of unexpected problems that often occur after you have deployed your performance improvement to production. You should always define a performance test suite that tests the whole application, and run it before and after you worked on a performance improvement.
These additional test runs will help you to identify the functional and performance side effects of your change and make sure that you don’t ship an update that caused more harm than good. That is especially important if you work on components that are used by several different parts of your application, like databases or caches.
4. Work on the Biggest Bottleneck First
And after you have created your test suite and analyzed your application with a profiler, you have a list of issues you want to address to improve the performance. That’s good, but it still doesn’t answer the question where you should start. You could focus on the quick wins, or start with the most significant issue.
It might be tempting to start with the quick wins because you will be able to show first results soon. Sometimes, that might be necessary to convince other team members or your management that the performance analysis was worth the effort.
But in general, I recommend starting at the top and begin work on the most significant performance problem first. That will provide you with the biggest performance improvement, and you might not need to fix more than a few of these issues to fulfill your performance requirements.
Enough about general performance tuning tips. Let’s take a closer look at some Java-specific ones.
5. Use StringBuilder 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.
So, which approach should you prefer?
The answer depends on the code that concatenates the String. If you’re programmatically adding new content to your String, e.g., in a for-loop, you should use the StringBuilder. It’s easy to use and provides better performance than StringBuffer. But please keep in mind, that the StringBuilder, in contrast to StringBuffer, is not thread-safe and might not be a good fit for all use cases.
You just need to instantiate a new StringBuilder and call the append method to add a new part to the String. And when you’ve added all parts, you can call the toString() method to retrieve the concatenated String.
The following code snippet shows a simple example. During each iteration, this loop converts i into a String and adds it together with a space to the StringBuilder sb. So, in the end, this code writes “This is a test0 1 2 3 4 5 6 7 8 9” to the log file.
StringBuilder sb = new StringBuilder(“This is a test”);
for (int i=0; i<10; i++) {
sb.append(i);
sb.append(” “);
}
log.info(sb.toString());
As you can see in the code snippet, you can provide the first element of your String to the constructor method. That will create a new StringBuilder containing the provided String and a capacity for 16 additional characters. When you add more characters to the StringBuilder, your JVM will dynamically increase the size of the StringBuilder.
If you already know how many characters your String will contain, you can provide that number to different constructor method to instantiate a StringBuilder with the defined capacity. That improves its efficiency even further because it doesn’t need to dynamical extend its capacity.
6. Use + 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.
In these cases, you should follow tip number 5 and use a StringBuilder.
But that’s not the case if you’re just breaking a String into multiple lines to improve the readability of your code.
Query q = em.createQuery(“SELECT a.id, a.firstName, a.lastName ”
+ “FROM Author a ”
+ “WHERE a.id = :id”);
In these situations, you should concatenate your Strings with a simple +. Your Java compiler will optimize this and perform the concatenation at compile time. So, at runtime, your code will just use 1 String, and no concatenation will be required.
7. Use 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.
8. Try to Avoid 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. So, better think twice if you need the additional precision, or if your numbers will exceed the range of a long. This might be the only thing you need to change to fix your performance problems, especially if you’re implementing a mathematical algorithm.
9. Check the Current Log Level First
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.
Here are two examples of how you should NOT do it.
// don’t do this
log.debug(“User [” + userName + “] called method X with [” + i + “]”);
// or this
log.debug(String.format(“User [%s] called method X with [%d]”, userName, i));
In both cases, you will perform all required steps to create the log message without knowing if your logging framework will use the log message. It’s better to check the current log level first before you create the debug message.
// do this
if (log.isDebugEnabled()) {
log.debug(“User [” + userName + “] called method X with [” + i + “]”);
}
10. Use Apache Commons StringUtils.Replace Instead of String.replace
In general, the String.replace method works fine and is pretty efficient, especially if you’re using Java 9. 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.
One candidate is Apache Commons Lang’s StringUtils.replace method. As Lukas Eder described in one of his recent blog posts, it dramatically outperforms Java 8’s String.replace method.
And it just requires a minimal change. You need to add a Maven dependency for the Apache’s Commons Lang project to your application pom.xml, and replace all calls of the String.replace method with the StringUtils.replace method.
// replace this
test.replace(“test”, “simple test”);
// with this
StringUtils.replace(test, “test”, “simple test”);
11. Cache Expensive Resources, Like Database Connections
Caching is a popular solution to avoid the repeated execution of expensive or frequently used code snippets. The general idea is simple: Reusing such resources is cheaper than creating a new one over and over again.
A typical example is caching database connections in a pool. The creation of a new connection takes time, which you can avoid if you reuse an existing connection.
You can also find other examples in the Java language itself. The valueOf method of the Integer class, for example, caches the values between -128 and 127. You might say that the creation of a new Integer isn’t too expensive, but it’s used so often that the caching of the most used values provides a performance benefit.
But when you think about caching, please keep in mind that your caching implementation also creates an overhead. You need to spend additional memory to store the reusable resources, and you might need to manage your cache to make the resources accessible or to remove outdated ones.
So, before you start caching any resources, make sure that you use them often enough to outweigh the overhead of your cache implementation.
Summary
As you’ve seen, it sometimes doesn’t require a lot of work to improve the performance of your application. Most of the recommendations in this post just need a small additional effort to apply them to your code.
But as usual, the most important recommendations are language-independent:
- Don’t optimize before you know it’s necessary
- Use a profiler to find the real bottleneck
- Work on the biggest bottleneck first
Published at DZone with permission of Thorben Janssen, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments