Java - Performance Tuning

Table of Contents

It's tricky to write well-performing applications, no matter which language or platform you use, and Java is no exception. Java performance tuning poses its own inherent difficulties, in addition to the popular issues. Think of the double-edged sword that is garbage collection, for a brief example. That doesn't mean it's a losing fight to optimize your software, nor that you need to be an expert to do so. There are some guidelines and best practices that are easy to follow and that will help you build a well-performing application.

Optimizing performance also will improve these:

  • Latency, enhancing user experience.
  • Application adoption, multiplying overall ROI.
  • Code quality, resulting in reduced downtime.
  • Server efficiency, reducing total infrastructure costs.
  • SEO, increasing search rankings.

The following Tips are usefull to optimize your Java applications.

Don’t optimize before you know it’s necessary

That might be one of the most important tuning tips for performance. You should follow common best practices and attempt to efficiently implement your use cases.

But that doesn't mean you're supposed to replace any standard libraries or build complex optimizations before you prove it needed.

Premature optimization takes a lot of time in most cases, and makes the code difficult to read and maintain.

And to make it even worse, since you spend a lot of time optimizing non-critical parts of your application, these optimizations most often do not provide any advantages.

So, how do you prove there is something you need to optimize?

First, you need to define how fast your application code has to be, e.g. by specifying the maximum response time for all API calls, or the number of records you want to import within a specified time frame.

You can measure which parts of your application are too slow after you've done that and need to be improved. And you ought to take a look at the second tip when you've done that.

To find the real bottleneck, use a profiler

Ask yourself where to start after you followed the first recommendation and identified the parts of your application you need to improve?

In two ways, you can approach this question:

  • You can look at your code and start with the piece that looks suspicious or where you feel it could cause problems.
  • Or you'll use a profiler and get detailed information about each part of your code's behavior and performance.

It should be obvious that the profiler-based method provides you with a better understanding of your code's performance implications and enables you to focus on the most critical parts.

And if you've ever used a profiler, you'll remember a few situations in which you've been surprised at parts of your code that created the performance problems. Had my first guess led me in the wrong direction more than once.

Create a performance test suite for the entire application

This is another general tip to help prevent many unexpected problems, often following the deployment of your production performance improvement.

You should always define a performance test suite that tests the whole application and run before and after performance improvement has been achieved.

These additional test runs help you identify the side effects of your changes on function and performance and make sure you do not update that causes more harm than good.

This is particularly important when you work on components that are used by various parts of your application, such as databases or caches.

First, work on the biggest bottleneck

And you have a list of issues that you want to resolve to improve the performance after you have created your test suite and analyzed your application with a profiler.

It's good, but the question where you should start still doesn't answer. You can concentrate on the fast win or start with the most important issue.

It might be tempting to start the fast wins because the first results will be shown soon. Sometimes, it may be necessary to persuade other team members or management that the analysis of performance is worthwhile.

But generally speaking I recommend starting from top to top and start working on the most important performance problem.

This will bring you the greatest improvement in performance and you may not have to address more than a few problems to meet your performance requirements.

Sufficient tips on general performance tuning. Let 's examine some of the Java-specific.

Use StringBuilder to programmatically concatenate strings

There are many different options for combining strings in Java. For example, you can use a simple StringBuffer or StringBuilder.

The answer depends on the code of the string. If you add new content to your string programmatically, e.g. in a for-loop, you should use the StringBuilder.

It is easy to use and performs better than StringBuffer. Please note, however, that the StringBuilder, unlike StringBuffer, is not thread-safe and may not fit for all applications. Use a new StringBuilder and call the append method to add a new part to the string. And if all parts are added, you can call toString() to retrieve the concatenated string.

A simple example is shown in the following code snippet.

StringBuilder sb = new StringBuilder(“This is a test”);
for (int i=0; i<10; i++) {
sb.append(” “);
// Output "This is a test0 1 2 3 4 5 6 7 8 9"

As you will see in the code snippet, you can supply the building method with the first element of your string. This will create a new StringBuilder with the provided string and 16 additional characters. When you add more StringBuilder characters, your JVM increases the size of the StringBuilder dynamically.

You can provide this number for a different builder method if you know how many characters your string will contain, to instantiate a StringBuilder with the defined capacity. This further improves its efficiency, as its capacity does not have to be dynamically extended.

Use "+" to concatenate strings

When you first implemented your Java application, someone would probably tell you not to concatenate Strings with `+. And that's right if you combine Strings in the logic of your application.

Strings are unchanged, and a new string object is stored in the result of each string concatenation. This requires additional memory and slows your application down, in particular if you combine multiple strings, and you should use a StringBuilder.

This is not the case, however, if you break a string into several lines to improve the readability of your code.

Query q = em.createQuery(“SELECT, a.firstName, a.lastName “
+ “FROM Author a “
+ “WHERE = :id”);

You should concatenate your strings with a simple + in these situations. This is optimized by your Java compiler and concatenation at compile time. So, your code will only use 1 string at runtime and no concatenation is needed.

Use primitives as much as possible

Another fast and easy way to avoid overheads and improve application performance is to use primitive types instead of their wrapper classes.

Thus, it's better to use an int rather than an integer or a double rather than a Double. This enables your JVM to store the stack value instead of the heap to reduce memory consumption and to handle it more efficiently in general.

Try to avoid BigInteger and BigDecimal

As we already talk about data types, BigInteger and BigDecimal should also be examined quickly. The latter is particularly popular because of its accuracy. But that's a price.

BigInteger and BigDecimal require a lot more storage than a single long or double and dramatically slow down all calculations.

Think, then, twice if you need additional accuracy or if your numbers exceed a long range.

That may be the only thing you need to change, particularly if you implement a mathematical algorithm, to fix your performance problems.

First check the current log level

This recommendation should be clear, but you can unfortunately find a lot of code that ignores it. Before creating a debug message, the current log level should always be checked first.

Otherwise, with your log message you could create a string that will be ignored later on.

Here are two examples of how you shouldn't.

Here are 2 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 + “]”);

Instead of String.replace, use Apache Commons StringUtils

The String.replace method generally works well and is quite effective , especially if you use Java 9.

However, if your application requires many replacement operations and you haven't updated to the latest version of Java, it's still worth checking for quicker and more efficient solutions.

One candidate is the StringUtils.replace method of Apache Commons Lang. The Java 8 String.replace method is dramatically outperformed.

And it only needs a minimal change. You have to add a Maven dependency to your pom.xml application for the Apache Commons Lang project and substitute all String.replace calls for StringUtils.replace method.

// replace this
test.replace(“test”, “simple test”);

// with this
StringUtils.replace(test, “test”, “simple test”);

Cache costly resources, such as database connections

Caching is a popular way of avoiding repeated running costly or often used code snippets.

The general idea is simple: it is cheaper to reuse such resources than to create a new one over and over again.

A typical example is the caching of database links in a pool. It takes time to create a new connection, which can be avoided if you reuse an existing connection.

Other examples can also be found in the Java language itself. For example, the valueOf method of the Integer class caches the values from -128 to 127.

You could say that the development of a new integer is not too expensive, but it is used often enough to provide a performance benefit when caching the most used values. However, when thinking of caching, please remember that your caching application also creates an overhead. You need to spend additional memory to store reusable resources, and you may need to manage your cache to access or delete outdated resources.

Therefore make sure you use them often enough to overweight the overhead of your cache implementation before caching any resources.

  • Exercise: An anagram is a word or a phrase made by transposing the letters of another word or phrase; for example, "parliament" is an anagram of "partial men," and "software" is an anagram of "swear oft." Write a program that figures out whether one string is an anagram of another string. The program should ignore white space and punctuation.