Original News Best Practices for Writing High-Performance Java Code

Quote
Original : How to Improve the Performance of a Java Application
Author: Eugen Paraschiv
Translation: Yan Jinghan

Abstract : This article first introduces load testing, application and server monitoring based on APM tools, and then introduces some of the ways to write high-performance Java code Best Practices. Finally, JVM-specific tuning techniques, database-side optimizations, and architectural tweaks are investigated. Below is the translation.

Introduction

In this article, we will discuss several methods that can help improve the performance of Java applications. We'll start with how to define measurable performance metrics, and then look at what tools are available to measure and monitor application performance and identify performance bottlenecks.

We will also see some common Java code optimization methods and best coding practices. Finally, we'll look at JVM tuning tips and architectural tweaks for improving Java application performance.

Note that performance optimization is a broad topic and this article is just a starting point for exploring the JVM.

Performance Metrics

Before we start optimizing the performance of an application, we need to understand the non-functional requirements such as scalability, performance, availability, etc.

Here are some performance metrics commonly used for typical web applications:
Average application response time Average number of concurrent users the
system must support Expected
requests per second during peak load
Analyzing performance bottlenecks and tuning performance plays a very important role.

sample application

We will use a simple Spring Boot web application as an example, which is covered in this post. This application can be used to manage employee lists and exposes a REST API for adding and retrieving employees.

We will use this program as a reference to run load tests and monitor various application metrics in the next chapters.

Identify performance bottlenecks

Load testing tools and application performance management (APM) solutions are commonly used to track and optimize the performance of Java applications. To find performance bottlenecks, the main thing is to load test various application scenarios, and use APM tools to monitor the usage of CPU, IO, and heap, etc.

Gatling is one of the best tools for load testing. It provides support for the HTTP protocol and is an excellent choice for HTTP server load testing.

Stackify's Retrace is a full-fledged APM solution. It's feature-rich and helpful in determining an application's performance baseline. One of the key components of Retrace is its code analysis capability, which is able to gather runtime information without slowing down the application.

Retrace also provides widgets for monitoring memory, threads, and classes of JVM-based applications. In addition to metrics from the application itself, it supports monitoring the CPU and IO usage of the server hosting the application.

Therefore, a full-featured monitoring tool like Retrace is the first step to unlocking the performance potential of your application. The second step is to reproduce real usage scenarios and loads on your system.

It's easier said than done, and it's important to understand the current performance of your application. That's what we're going to focus on next.

Gatling Load Testing

Gatling's mock test scripts are written in Scala, but the tool also comes with a very useful graphical interface that can be used to record specific scenarios and generate Scala scripts.

After running the simulation script, Gatling generates a very useful HTML report that can be used for analysis.

Defining the scene

Before starting the recorder, we need to define a scene that represents what happens when the user browses the web application.

In our example, the specific scenario would be "Starting 200 users, each making 10,000 requests."

Configuring the Recorder

As described in "First Steps in Gatling", use the following code to create a file called Scala file of EmployeeSimulation:
code
class EmployeeSimulation extends Simulation { 
    val scn = scenario("FetchEmployees").repeat(10000) { 
        exec( 
          http("GetEmployees-API") 
            .get("http://localhost:8080/employees" ) 
            .check(status.is(200)) 
        ) 
    } 
    setUp(scn.users(200).ramp(100)) 


Running the load test

To execute the load test, run the following command:
code
$GATLING_HOME/bin/gatling.sh-sbasic.EmployeeSimulation 

Load testing of the application's API can help to find very subtle and hard-to-find bugs such as exhausted database connections, request timeouts under high load, due to memory High heap usage due to leaks, etc.

Monitoring applications

To use Retrace for Java application development, you first need to apply for a free trial account on Stackify. Then, configure our own Spring Boot application as a Linux service. We also need to install the Retrace agent on the server hosting the application, as described in this post.

After the Retrace agent and the Java application to be monitored are started, we can go to the Retrace dashboard and click the AddApp button to add the application. After adding the application, Retrace will start monitoring the application.

Find the slowest point

Retrace automatically monitors applications and tracks usage of dozens of common frameworks and their dependencies, including SQL, MongoDB, Redis, Elasticsearch, and more. Retrace can help us quickly determine why the application has the following performance problems: Is
a certain SQL statement slowing down the system?
Is Redis suddenly slow?
Is a specific HTTP web service down, or is it slow?
For example, the graph below shows the slowest component over a given period of time.


Code-level optimizations

Load testing and application monitoring are useful for identifying some of the key performance bottlenecks of an application. But at the same time, we need to follow good coding habits to avoid too many performance issues when monitoring the application.

In the next chapter, we'll look at some best practices.

Using StringBuilder to concatenate strings

String concatenation is a very common operation and an inefficient operation. Simply put, the problem with using += to append strings is that each operation allocates a new String.

The following example is a simplified but typical loop. The original connection method is used in the front, and the builder is used in the back:
code
public String stringAppendLoop() { 
    String s = ""; 
    for (int i = 0; i < 10000; i++) { 
        if (s.length() > 0 ) 
            s += ", "; 
        s += "bar"; 
    } 
    return s; 

 
public String stringAppendBuilderLoop() { 
    StringBuilder sb = new StringBuilder(); 
    for (int i = 0; i < 10000; i++) { 
        if ( sb.length() > 0) 
            sb.append(", "); 
        sb.append("bar"); 

    return sb.toString(); 


The StringBuilder used in the above code is very effective in improving performance. Note that modern JVMs optimize string operations at compile or runtime.

Avoid recursion Recursive code logic that

causes StackOverFlowError errors is another common problem in Java applications. If you can't get rid of the recursive logic, then tail recursion would be better as an alternative.

Let's look at an example of head recursion: the
code
public int factorial(int n) { 
    if (n == 0) { 
        return 1; 
    } else { 
        return n * factorial(n - 1); 
    } 


Now let's rewrite it For tail recursion:
code
private int factorial(int n, int accum) { 
    if (n == 0) { 
        return accum; 
    } else { 
        return factorial(n - 1, accum * n); 
    } 

public int factorial(int n) { 
    return factorial(n, 1); 


Other JVM languages ​​(such as Scala) already support the optimization of tail-recursive code at the compiler level, and of course, there is some controversy about this optimization.

Use Regular Expressions Carefully

Regular are useful in many scenarios, but they tend to have a very high performance cost. It is important to understand the various JDK string methods that use regular expressions, such as String.replaceAll(), String.split().

If you have to use regular expressions in computationally intensive code sections, you need to cache Pattern references to avoid repeated compilation:
code
static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*" ); 

using some popular libraries like Apache Commons Lang is also a good option, especially for manipulation of strings.

Avoid creating and destroying too many

threads The creation and disposal of threads is a common cause of performance problems for the JVM because the creation and destruction of thread objects is relatively heavy.

If the application uses a lot of threads, then using a thread pool is more useful because thread pools allow these expensive objects to be reused.

To this end, Java's ExecutorService is the basis for thread pools, providing a high-level API to define the semantics of and interact with thread pools.

The Fork/Join framework in Java 7 is also worth mentioning because it provides tools to try all available processor cores to help speed up parallel processing. In order to improve the efficiency of parallel execution, the framework uses a thread pool called ForkJoinPool to manage worker threads.

JVM Tuning
Heap Size Tuning

Determining an appropriate JVM heap size for a production system is not an easy task. The first step to do is to predict memory requirements by answering the following questions:
How many different applications are planned to be deployed into a single JVM process, e.g. what is the number of EAR files, WAR files, jar files?
How many Java classes might be loaded at runtime, including classes from third-party APIs?
Estimate the space required for in-memory caches, for example, internal cache data structures loaded by applications (and third-party APIs), such as data cached from databases, data read from files, and so on.
Estimate the number of threads the application will create.
These numbers are hard to estimate without being tested in real-world scenarios.

The best and most reliable way to get information about your application's requirements is to perform actual load tests on your application and track performance metrics at runtime. The Gatling-based tests we discussed earlier are a good way to do this.

Choosing the Right Garbage Collector

Stop-the-world (STW) garbage collection cycles are a big issue that affects the responsiveness and overall Java performance of most client-facing applications. However, current garbage collectors mostly solve this problem, and with proper optimization and sizing, are able to eliminate the awareness of collection cycles.

Profilers, heap dumps, and detailed GC logging tools help with this. Again, note that these need to be monitored under real-world load patterns.

For more information on the different garbage collectors, check out this guide.

JDBC performance

Relational databases are another common performance issue in Java applications. In order to get the response time for a full request, we naturally have to look at every layer of the application and think about how to get the code to interact with the underlying SQL DB.

Connection Pooling

Let 's start with the well-known fact that database connections are expensive. The connection pooling mechanism is a very important first step in solving this problem.

HikariCP JDBC is recommended here, which is a very lightweight (about 130Kb) and extremely fast JDBC connection pooling framework.

JDBC Batch

Persistence processing should perform batch operations whenever possible. JDBC batching allows us to send multiple SQL statements in a single database interaction.

In this way, performance can be significantly improved, both on the driver side and on the database side. * PreparedStatement* is a great batch command, some database systems (eg Oracle) only support batching of prepared statements.

Hibernate, on the other hand, is more flexible, allowing us to quickly switch to batch operations with only one configuration change.

Statement Caching

Statement caching is another way to improve the performance of your persistence layer, a little-known but easy-to-master performance optimization.

You can cache PreparedStatements on the client side (driver) or database side (syntax tree or even execution plan) as long as the underlying JDBC driver supports it.

Scaling

Database replication and sharding are great ways to increase throughput, and we should take advantage of these tried and true architectural patterns to scale the persistence layer of enterprise applications.

Architecture Improvements
Caching

The price of memory is now low and getting lower, and the performance cost of retrieving data from disk or over the network is still high. Caching has naturally become a key factor that cannot be ignored when it comes to application performance.

Of course, introducing a separate caching system into the application's topology does increase the complexity of the architecture, so one should take full advantage of the existing caching capabilities of the libraries and frameworks currently in use.

For example, most persistence frameworks support caching. Web frameworks such as Spring MVC can also use the caching support built into Spring, as well as the powerful HTTP-level caching based on ETags.

Scale out

No matter how much hardware we have in a single instance, there will be times when it will not be enough. In short, scaling has inherent limitations, and when a system encounters these problems, scaling out is the only way to handle more load. This step is sure to be quite complicated, but it is the only way to scale the application.

For most modern frameworks and libraries, this is still well supported and will get better. The Spring ecosystem has a complete set of projects dedicated to addressing this specific area of ​​application architecture, and most other frameworks have similar support.

In addition to improving Java performance, scaling horizontally through a cluster also has other benefits. Adding new nodes can create redundancy and better handle failures, thereby increasing the availability of the entire system.

Conclusion

In this article, we explored many concepts around improving the performance of Java applications. We started with an introduction to load testing, application and server monitoring based on APM tools, followed by some best practices for writing high-performance Java code. Finally, we looked at JVM-specific tuning techniques, database-side optimizations, and architectural tweaks.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326255845&siteId=291194637
Recommended