Java performance is an issue of interest for all Java application developers, since making an application fast is as important as making it functional. Steven Haines uses his personal experience on Java performance issues to conclude that most of them have common root causes. So, as a performance analyst, Haines sorts the basic performance issues to three basic categories:
Database problems, that mostly have to do with persistence configuration, caching or database connection thread pool configuration.
Memory problems, that usually are garbage collection misconfiguration or memory leaks.
Concurrency problems, and basically deadlocks, gridlocks and thread pool configuration problems.
Let’s delve into each category…
Since database is the basic component of an application functionality, it also is the basic root of performance issues. Problems may occur due to wrong use of access to the database, bad connection pool size or missing tuning.
Even though today Hibernate and other JPA implementations provide fine tuning of database access, there are some more options such as eager or lazy fetching, that may lead to long response times and database overheads. Eager fetching make less but more complex database calls, whereas lazy fetching makes more but more simple and fast database calls.
Problems occur when the load of the application increases and it causes a much bigger database load. So, in order to fix this, you can take a look at the business transaction counters, the database counters, but basically at the correlation between a business transaction and database calls. To avoid such problems you must understand well the persistence technology used, set correctly all configuration options, so as to pair their functionality with your business domain needs.
Caching has optimized the performance of applications, since in-memory data is faster to access than persisted ones. Problems are caused when no caching is used, so every time a resource is needed it is retrieved from database. When caching is used, problems occur due to its bad configuration. Basic things to notice here are the fixed size of a cache and the distributed cache configuration. Cached objects are stateful, unlike pools that provide stateless objects. So a cache must be properly configured so as not to exhaust memory. But what if a removed object is requested again? This ‘miss’ ratio must be configured in cache settings, along with the memory.
Distributed caching may also cause problems. Synchronization is necessary when caches are set to multiple servers. Thus, a cache update is propagated to caches in all servers. This is how consistency is achieved, but it is a very expensive procedure. When caching is used correctly the application load increase does not increase the database load, but when the caching settings are wrong, then the database load increases, causing CPU overhead an even disk I/O rate.
In order to troubleshoot this problem you should first examine the database performance so as to decide if cache is needed or not. Then, you should determine the cache size, using the hit ratio and miss ratio metrics. You can avoid facing caching problems though, by planning correctly your application before building it. Make sure to use serialization and techniques that provide a scalable application.
Pool connections are usually created before starting the application, since they are expensive to create. A pool of connections is shared across the transactions and the pool size limits the database load.
Pool size is important. Not enough connections make business transactions to wait and the database is under-utilized. On the other hand, too many connections cause bigger response time and database overload. In order to solve this problem you must check whether your application is waiting for a new connection or for a database query to be executed. You can always avoid it though, by optimising the database, test the application with different pool size to check which one fits the case.
Memory problems have to do with Garbage Collector and memory leaks.
Garbage collection may cause all threads to stop in order to reclaim memory. When this procedure takes too much time or occurs too often, then there is a problem. Its basic symptoms are the CPU spikes and big response times. To solve this you can configure your
-verbosegc params, use a performance monitoring tool to find major GC occurs, and a tool to monitor heap usage and possible CPU spikes. It is almost impossible to avoid this problem, though can limit it by configuring heap size and cycling your JVM.
Memory leaks in Java may occur in different ways than C or C++, since they are more of a reference management issue. In Java a reference to an object may be maintained even though it may not be used again. This may lead to an OutOfMemory error and demand a JVM restart. When the memory usage is increased and the heap runs out of memory then the memory leak issue has occurred. To solve it, you could configure the JVM params properly. To avoid having to deal with memory leaks, you can pay attention while coding to memory leak – sensitive Java collections, or session management. You can share memory leaks avoid tips with colleagues, have an expert take a look at your application code, and use tools to avoid memory leaks and analyze heap.
Concurrency occurs when several computations are executed at the same time. Java uses synchronization and locks to manage multithreading. But synchronization can cause thread deadlocks, gridlocks and thread pool size issues.
Thread deadlocks occur when two or more threads are trying to access same resources and the one is waiting for the other one to release a resource and vice versa. When a deadlock occurs the JVM exhausts all threads and the application is getting slower. Deadlocks are very difficult to reproduce. So, a way to solve a deadlock problem is to capture a thread dump while two threads are deadlocked and examine stack traces of the threads. To avoid this problem you’d better make your application and its resources as immutable as possible, make use of synchronization and check for potential threads interactions.
Thread gridlocks may occur when too much synchronization is used and thus too much time is spent waiting for a single resource. To notice this, you must have both slow response times and low CPU utilization, since many threads try to access the same code part and they are waiting for the one that has it to finish. So, how can you solve this? You must first check where your threads are waiting and why. Then, you should eliminate the synchronization requirements according to your business requirements.
Thread pool configuration locks
When an application uses an application server or a web container, a thread pool is used to control the concurrently processed requests. If this thread pool is small, then the requests will wait a lot, but if it is too large, then the processing resources will be too busy. So, at a small pool size the CPU is underutilized but the thread pool utilization is 100%, whereas at a large pool size the CPU is very busy.
You can troubleshoot this problem easily, by checking your thread pool utilization and CPU utilization and decide whether to increase or decrease the pool size. To avoid it, you must tune the thread pool, and that is not so easy to do.
Finally, two basic issues that may occur are the performance issue to be an afterthought, or the performance issue to be noticed by the end users.
The first case is a common problem. Usually developers create an application that is functional but fails in performance tests. To solve this they usually have to make an architectural review of the application, where performance analysis tools seem very handy. To avoid this problem, try to test performance while developing the application, so continuous integration is the key.
For the second case, what happens when end users of the application inform you that there are performance issues? There are tools to avoid this case, such as JMX to check your servers behavior. Business Transaction Performance results combined with JMX results may help too. Method-level response time checks all methods called in a business transaction and finds hotspots of the application. So, you’d better make use of one of these tools, so that end users will never alert you for performance.
Interested to learn more? Then you should download the relevant ebook here.