Common elements in your Java code that may be consuming excessive memory
If you are facing memory related issues in Java, these are some of the things you should be keeping in mind to not misuse your application’s memory.
- Usage of ArrayList<> :
If you are using List to store your elements, then you might be using ArrayList in Java. While there is nothing wrong with using ArrayList, you should understand how ArrayList works. ArrayList in Java is backed by an Array as the name suggests and most importantly, ArrayList is mutable. This means your ArrayList has the potential huge data, which is a double edged sword. This could mean that you can use when you do not know the size and if the size is expected to be not so too big. On the flip side, this could also mean that this ArrayList, when used as a global variable has an open door to accept n number of elements which could very much hog up your memory.
// This should be carefully analyzed, especially while storing heavy objects
List<HeavyObjectDTO> objectCollection = new ArrayList<>();
Tip: If you do know the size of the list before hand, then using Java 9’s List.Of() would be very helpful as this is immutable.
// This should be used when size is pre-determined
List<String> movies = List.Of("Fight Club", "The Batman", "The Gentlemen");
2. Poor SQL query usage:
This is a very crucial one. Designing a Table/Database effectively is very important to fetch the data seamlessly. What is even more important that how and how frequently you fetch the data. This plays a huge role in your application’s memory usage.
Here is an example: You have a table which stores heavy data in a particular column. What most people do here is only fetch the required columns to avoid slow queries. But it is important to weigh in the effects of querying those required columns as well. If average size of the said heavy object is 10MB, and you are querying the data very frequently and especially querying using by a list of Ids or set of Ids, this is a memory killer.
Consider query that would fetch by 100 Ids, so 100 * 10MB = 1GB. If this query is executed even 5 times, then 1GB * 5 = 5 GB just to fetch the data.
While the example might be exaggerated a bit, you get the point.
3. Conundrum of writing “clean code”:
As Software Developers, we take pride in writing clean code. I understand this as code is not only something you write to make your application work but it gives a sense of joy when you write good clean code (which is an art, by the way) which reads like a story book and it is a part of your legacy. I mean who doesn’t like good code right?
But there are trade-offs for writing clean code as well. When you solely focus on the readability of your code, then there is a good chance that this might backfire. Consider this example:
We’ll take the reference of previous example’s heavy object from SQL query:
List<HeavyObjectDTO> processHeavyObjects() {
List<HeavyObjectDTO> heavyObjects1 = db.fetchHeavyObjects(...);
List<HeavyObjectDTO> heavyObjects2 = db.fetchHeavyObjects(...);
List<HeavyObjectDTO> heavyObjects3 = db.fetchHeavyObjects(...);
List<HeavyObjectDTO> combinedList = new ArrayList<>(heavyObjects1);
combinedList.addAll(heavyObjects2);
combinedList.addAll(heavyObjects3);
return combinedList;
}
While it is readable, this should not be entertained at all. Just the blind usage of lists here would consume a lot of memory. If this is function is called elsewhere, then the lists would be stored in heap memory. This could be written in a better way, like below:
List<HeavyObjectDTO> processHeavyObjects() {
return Stream.of(
db.fetchHeavyObjects(...),
db.fetchHeavyObjects(...),
db.fetchHeavyObjects(...)
)
.flatMap(List::stream)
.collect(Collectors.toList());
}
4. Loading/processing heavy objects in memory:
When you have a poor database design and you need to fetch objects from database and process in code, then this will definitely take a toll on your application’s memory.
For example, you are doing a fetchById() database call which returns a list of objects.
This below piece of code will consume a lot of memory:
public HeavyObjectDTO getMaxDateRecord(List<HeavyObjectDTO> records) {
return records.stream()
.max((o1, o2) -> o1.getDate().compareTo(o2.getDate()))
.orElse(null);
This code is just fetching the latest record by date.
To make it memory efficient, here’s what could be done:
Rather than processing in memory, just fetch the required record ONLY.
Example:
SELECT *
FROM heavy_objects
WHERE (date, id) = (
SELECT MAX(date), MAX(id)
FROM heavy_objects
WHERE date = (SELECT MAX(date) FROM heavy_objects)
);
OR
SELECT *
FROM heavy_objects
ORDER BY date DESC, id DESC
LIMIT 1;
5. Bad usage of Maps:
Just like in the example #1, a map in Java is mutable, which means that the door is open to accept a lot of elements when used as a global variable, which have potential downsides.
Map<String, HeavyObjectDTO> heavyMap = new HashMap<>();
heavyMap = getHeavyObjects(...);
If there is not a limit set to this, then this could hog the memory as well.