The trend towards cheaper and larger RAM chips over the last two decades has tended to cause developers to view memory as an infinite resource, and pay little attention to memory-saving techniques.
This attitude can lead to expensive system failures, excessive cloud computing costs, performance issues and unnecessary investment in bigger and better hardware to cope with a growing workload.
System design is always a trade-off, and we may sometimes be justified in opting for higher memory usage to achieve better processing speeds or simpler systems. Memory-hungry applications should never be simply the result of lazy development or lack of planning.
In this article, we’ll look at some of the common reasons why systems may use more memory than they actually need. Excessive memory usage is often caused by buggy programs that leak memory, but in many cases the problem is simply memory wastage. We’ll look at both of these scenarios.
Common Reasons for Excess Memory Usage
1. Failing to Release Objects for Garbage Collection When No Longer Needed
The garbage collector (GC) is a background process responsible for cleaning up the JVM memory. If you’re not familiar with what this entails, you may like to read this article: What is Garbage Collection?
GC removes from memory any items that are no longer pointed to by valid references. This happens when:
- An object is explicitly set to null
- An object goes out of scope. For example, when a method completes, all its local variables go out of scope.
Additionally, many classes, such as readers and writers, have a close() method. This releases any objects that are no longer needed.
Coding issues that can lead to a build-up of unneeded objects in memory include:
- Variables defined in the wrong scope. One example may be objects that are defined as class variables when they should actually be local variables within a method.
- Defining large variables as static.
- The close() method not being called, even though a class offers this method;
- Failing to set objects to null when they are no longer needed.
- Poorly-designed exception handling, where objects are not released when an exception occurs. It’s a good idea to include finally code with try blocks. This can be used to close or release objects used within the block.
2. Inefficient Caches and Collections
Caching is an effective performance-enhancing technique when used correctly. Frequently-used data is held in memory, rather than continually using slow input-output transfers to repeatedly read it from storage. However, it needs to be used properly, with sensible size limits. We also need to take care not to allow duplicate data to be cached. There must be a clearance policy to remove data from the cache if it’s in danger of growing too large.
Likewise, collections can be an efficient way to store data that needs to be used repeatedly, but they should be used with caution. Make sure duplicates can’t be added to collections, and items no longer in use are removed. Using the right type of collection for the task is also important. Since collections can grow very large, make sure they can be garbage collected when they’re no longer needed.
Also, bear in mind that collections often come with memory overheads. In simple cases, where we know how many objects we’re going to store, a simple array may be more efficient.
3. Loops That Fail to Terminate
These will normally be picked up in early testing, but occasionally an unusual condition can cause a program to loop indefinitely. Let’s look at an example that processes rows from a database using a ResultSet object. This code may work correctly for years, but if for any reason an exception is thrown during the processData() method, it will skip the next() command, the terminating condition will never become true and the program will loop indefinitely.
boolean moreRows=true;
while(moreRows) {
try {
// Complex method that adds data to a collection, creates
// objects and instantiates classes
processData()
moreRows=resultSet.next();
}
catch(Exception e) {
// Logs an alarm, but does not terminate the program
raiseAlert(e);
}
}
In fact, it need not be a never-ending loop: if a loop is ever iterated more times than the programmer intended, it can result in memory problems.
If the code within the loop creates objects or classes, or adds data to a collection, it can result in memory issues.
4. Loading Large Chunks of Information at Once
In the early planning stages, we need to be aware of exactly what data will be needed by the program at one time. Too often, programmers will load large amounts of data from a file, database or network and store it for processing, rather than planning the program so it reads and processes information line by line.
Choosing the right type of stream for the task can make a big difference to memory requirements. Taking full advantage of the sorting and filtering capabilities of a database can also cut down heap usage considerably.
It’s tempting to make buffer sizes very large to increase performance. However, if this is done indiscriminately, it can decrease performance due to overloading the garbage collector.
5. Wasted Memory
Many developers would be surprised to know how much memory can be saved by tightening up the code in a few important areas.
Typical reasons for memory wastage include:
- Boxed numbers. This refers to storing numbers as objects rather than Java primitives. For example, Integer x = new Integer(5) is a boxed number, whereas int x = 5 is a Java primitive. Primitives do not have headers, but all objects have a header. This occupies around 12 bytes, depending on the JVM version. This can soon add up, especially when using large arrays of numbers.
- Allowing collections to be sized automatically, rather than specifying an initial size and the amount of additional space to be allocated when it needs to expand. A HashMap, for example, defaults to an initial size of 16 buckets, and doubles in size each time it needs to expand. This can soon lead to a considerable amount of wasted space.
- Duplicate objects. The most common culprit here is duplicate strings. This article explains how to eliminate duplicate strings.
- Using string concatenation within a loop. Each concatenation creates a new String object. Instead, use the StringBuffer class for manipulating strings within a single object.
- Creating objects before they’re needed. Only create objects at the time and within the scope where they will be used.
Analyzing Memory Usage
It’s a good idea at each stage of the project lifecycle to monitor heap usage to proactively avoid memory issues.
Several good tools are available for analyzing heap dumps, including Eclipse MAT and HeapHero. These provide useful charts to show how memory is being used, such as the HeapHero chart below. This shows objects in descending order of size.

Fig: Largest Objects Displayed by HeapHero
HeapHero has the additional advantage that it shows a breakdown of detected memory wastage:

Fig: Wasted Memory Report Produced by HeapHero
This is further broken down with details in each category. An example is shown below.

Fig: Details of Duplicate Strings Produced by HeapHero
It’s also a good idea to regularly monitor garbage collection efficiency using a tool such as GCeasy.
Conclusion
Excess memory usage affects both performance and costs, and, when programming for small devices, it can be critical.
A few simple changes to an application can save huge cloud computing costs, and prevent sluggish response times and expensive system crashes.
Best practice is to take into account memory usage considerations from the planning stage onwards, and monitor the system regularly to prevent memory issues from developing.

Share your Thoughts!