Victoria - Fotolia
One of the first indicators of a runtime performance problem is a high Java CPU usage report from a JVM profiler or Java monitoring tool. Unfortunately, high Java CPU utilization problems on Windows and Linux are not always easy to resolve, as this metric is often a red herring for a problem that is peripheral to the CPU.
For example, if an application overzealously allocates instances, the garbage collector (GC) will be forced into action when object references go out of scope. Increasingly frequent GC cycles will not only trigger JVM stop-the-world events that make an application seem unresponsive, but they also cause high Java CPU utilization spikes. Rectification of the GC issue has nothing to do with implementing more efficient algorithms or logical workflows. The fix is to address the underlying object allocation issue that inefficiently uses memory and triggers needless GC.
Java CPU metrics can be misleading
Blocked threads with contention issues can also cause JVM profiler tools to report 100% Java CPU utilization. Concurrency problems and deadlocks aren't really a processor issue, but instead a problem with how threads are allocated, and the methods they access are synchronized or blocked.
CPU utilization can also be a misleading metric.
When a CPU is in an idle state, it reports its status as being unused, as it's not doing any work. However, when threads are blocked, they will put the CPU in a wait state. The CPU doesn't perform any logic when it's in the wait state, but it reports back to JVM profiling tools that it's busy, despite the fact that it's doing nothing.
Furthermore, just because your hardware reports 100% CPU utilization, don't assume that it's the JVM causing it. CPU usage might skyrocket when your application is under load, but that spike might be attributable to a system process or a misconfiguration of the software stack. If a server's virtual memory is misconfigured, page file thrashing will consume a majority of the CPU cycles. A virtual memory problem needs to be solved by the DevOps team or system admins. It's not a problem that's attributable to your app or how you've tuned the performance of the JVM.
Most common Java performance problems
The majority of JVM performance problems can be traced back to I/O operations, such as writes to the file system or interactions with a back-end relational database management system or message queue. A misconfigured database connection pool, where resources are constantly created and destroyed to provide Java Database Connectivity to Spring and JPA-based applications, can trigger high Java CPU usage rates. Poor I/O resource management can also lead to leaked memory, and an inevitable OutOfMemoryError.
Another misleading source of high Java CPU usage is a poorly designed RESTful API that makes too many network calls to other services. Chatty applications with a large number of HTTP requests, along with the associated overhead of parsing JSON and XML on each request-response cycle, will often trigger 100% Java CPU usage reports. This problem has become increasingly common in modern enterprise architectures, as developers re-architect software monoliths into microservices.
Peripheral causes of high Java CPU usage
When you troubleshoot high Java CPU usage problems, the first step is to eliminate the various red herrings mentioned above. To review, these peripherally related issues include:
- bad JVM memory management;
- poorly configured Java GC;
- issues more correctly attributable to the software stack;
- thread synchronization, contention and deadlock issues; and
- underlying file and database I/O problems.
Only after root-cause analysis eliminates these issues as a potential cause of the high Java CPU usage problem should time be taken to troubleshoot potential issues in the code.
Learn how to get started with Java Flight Recorder.
Direct causes of high Java CPU usage
What could possibly be the culprit when your Java code is putting too much stress on the CPU? The most common, directly attributable causes of high Java CPU usage problems include:
- inadvertent infinite loops;
- poorly designed workflows and inefficient algorithms;
- use of recursive logic;
- incorrectly chosen collection classes; and
- recalculation of already calculated values.
Be it a fencepost error or just sloppy development, it's not unheard of for a programmer to start a loop and incorrectly code the condition that breaks out of it. An infinite loop that does nothing but consume clock cycles is the result. If multiple threads hit this line of code, you have a multi-threaded application doing nothing but meaningless iterations. Eliminate the infinite loops and CPU usage should go back to normal.
Poorly written workflows and algorithms
The CPU executes logic. If an application includes poorly written workflows, and the code is wired together like a plate of spaghetti, then your CPU will devour needless clock cycles. Update commonly invoked workflows and rework poorly performing algorithms to get the most out of your CPU.
While some programming languages are optimized for recursive logic, Java isn't one of them. Recursive algorithms create threads that are hard to break out of, allocate object that are not easily reclaimed by garbage collection algorithms, and they create a tower of Java stack frames that are difficult to unwind. Throw in the looming threat of a StackOverflowError, and the case for iterative over recursive algorithms isn't a difficult one to make.
Poorly chosen collection classes
List processing lies at the heart of most enterprise applications. As such, developers have many collection classes to choose from. If a developer chooses to use a LinkedList instead of an ArrayList on a large data set, CPU utilization will go through the roof. Similarly, if a developer chooses to use the older Hashtable over a HashMap, synchronization may needlessly consume clock cycles. Choose the wrong Java collection class, and application performance will suffer. Choose the correct collection classes, and your high Java CPU usage problems will disappear.
Recalculation of already calculated values
It's not uncommon for a given value to be calculated numerous times throughout an application. If this is the case, hold the result of the first calculation in a variable and reference that variable on all future interactions. Small changes like this can have a significant impact on application performance, especially if cryptography, graphics manipulation or other CPU-intensive operations are involved.
With a good JVM profiler like Java Flight Recorder, and an analytics tool like JDK Mission Control tool available to inspect the results, identifying the culprit responsible for high Java CPU usage problems shouldn't be a problem. And once identified, finding a fix is just a matter of implementing new software routines and testing the results until the fix is in.
Dig Deeper on Development tools for continuous software delivery
Related Q&A from Cameron McKenzie
There's a reason why so many vendors have moved from monoliths to microservices. Here are 11 real benefits microservices bring to companies. Continue Reading
Not everyone is keen to adopt a cloud-native architecture, for good reasons. These common drawbacks to microservices might convince you to stick with... Continue Reading
Understand the difference between checked and unchecked exceptions in Java, and learn how to handle these problem conditions properly and gracefully. Continue Reading