Android developer? You must understand Memory Management and Garbage Collection
A couple of years back when I took a job as a Android developer for the first time, one of the things that frustrated me most with peer Android developers (or Java programmers) was how little they cared about managing memory. Having worked in the dark ages of C/C++ where as a programmer you had to take utmost care before calling any malloc
, new
, free
or delete
, all I got to hear was Java does automatic memory management and memory will be cleaned up in most optimal way. It is true to a great extent that Garbage Collection is very helpful as a programmer by helping you focus on writing functionality without worrying about when to deallocate and allocate memory. However, GC incurs a performance cost and if your app starts getting frequent GC calls you can end up seeing all the performance issues like frame rate dropping, long response times and also ANRs (App not responding). Before we see why GC can be bad for performance let us first look into the basics of how memory is managed in Java.
Stacks and Heaps
The managed memory environments in Android, whether it is ART (post KitKat) or Dalvik virtual machine, basically keep track of each memory allocation. While the way in which both ART and Dalvik handle GC varies with ART being much improved at it, the basics of allocation remains almost same.
In layman’s term whatever you do in your program needs to be stored in physical memory(RAM). Every interface, class, object and running method has a separate region of memory to keep track of variable values and other related information. There are two places memory can be allocated, namely stack and heap.
Stack as name suggests is memory which is referenced in LIFO (Last in, First out) order. It is used to store method specific values that are short-lived and references to other objects in the heap that are getting referred from the method. Whenever a method is invoked, a new block is created in the stack memory for the method to hold local primitive values and reference to other objects in the method.As soon as method ends, the block becomes unused and become available for next method. Generally, stack is never the cause of memory concerns and cleaning the stack memory is inexpensive to CPU cycles.
Now, we come to major player, namely the Heap. In one line, every time you call a new XYZ(***)
you allocate memory on the heap, i.e, Whenever we create a object it gets allocated on the Heap. Heap space is used by runtime to allocate memory to Objects and JRE classes. So, whenever we talk about Memory Management we are actually meaning managing the Heap Space.
Memory management is no Magic
The crux behind automatically managing memory is finding out the pieces of memory which are no longer being used by program and freeing them from the Heap. This is handled by your friend called Garbage Collector. So primarily, the job of Garbage Collector is to
- Find all objects no longer in use by program.
- Free up all the space occupied by the objects.
Two questions that might me coming into your mind may be
- How often or when does GC run.
- How does it determine which objects are no longer in need.
To answer the second question, they way it determines when an object is not in need is called Reference Counting. It basically implies keeping a count of number of references to the object, and if zero it means that memory can be cleaned. (To know more about it refer to the link)
And to answer the first question as to when GC runs the short answer is when system feels it running short of memory. Actually, heap is a collection of buckets and objects are allocated into these buckets based on the expected life and size of an object being allocated. Now, whenever the runtime finds one of these buckets running out it gives a call to its friend, the Garbage Collector. For further reading on how this works refer the documentation
Wait? Isn’t Garbage Collection cycle a good thing?
Well, yes and no. ‘Yes’, because it clears the memory in time so that your app does not crash trying to allocate non-existing memory. And ‘No’, because as a programmer you don’t have much idea or control as to when GC will fire up. Imagine you are rendering a beautiful canvas animation and a sudden GC frame comes up, and as a result your animation stutters. Although in most cases GC is very fast (specially on the ART), it can still hamper your app’s performance, more so, if you have lot of objects to reference count and deallocate. Now, imagine if you end up having a lot of GC cycles firing up in a small session. It will mean your app freezing up quite badly.
Great, how do I ensure to keep GC cycles to minimum while my app runs and also that GC cycles are short
Generate less garbage. Be judicious when creating new objects. Avoid doing too much allocation while doing CPU intensive tasks like animation or bitmap rendering. Free up references as and when needed. Avoid any memory leaks.
Too confusing. How do I start?
Android profiler is a good place to start. Use a device with minimal resources. One of the challenges faced by Android developer is that there are too many hardware and software configurations to consider and to top it, there are different flavors to Android kernel as well which makes benchmarking performance difficult. However, taking a heap snapshot when you see heap memory growing up should be a good starting point to understand your performance problem. To know more see the documentation
I do not have any memory problem right now. But can I still be making mistakes in managing memory? What are common mistakes?
You are right. Absence of symptoms is not necessarily the absence of disease. The symptoms generally appear later and so it is a good practice to consider about memory optimization whenever you code. Let us see some examples where we may be making such mistakes
1. Creating new objects when old could have worked
void sendAllToAnalytics(Context context, List<Event> events) {
for (Event event : events) {
new AnalyticsHelper(context).send(event);
}
}
I have lost count how many times I have seen this mistake. Did you spot it? Let’s say the list had 1000 elements, so you would end up creating 1000 objects of AnalyticsHelper
in this loop. Now let's say each object needs 1KB of memory, you would have created a heap allocation of around 1 MB. This may not sound much at first but imagine a low end device which is already rendering a large image or storing a large bitmap. I mean what's wrong with
void sendAllToAnalytics(Context context, List<Event> events) {
AnalyticsHelper helper = new AnalyticsHelper(context);
for (Event event : events) {
helper.send(event);
}
}
Let’s see another example. Here we are drawing few shapes on canvas
.....
public class WrongView extends View {
int x1,x2, y1,y2;
ValueAnimator animator;
int alpha;
...... public WrongView(Context context) {
super(context);
x1 = 0; y1 = 0; x2 = 100; y2 = 100; alpha = 0;
animator = ValueAnimator.ofFloat(0f, 0.3f);
animator.addUpdateListener(animation -> {
float duration = (float)animation.getAnimatedValue();
alpha = (int) (256 * duration);
});
animator.start()
}
.....
@Override
protected void onDraw(Canvas canvas) {
drawShape();
postInvalidateDelayed(1000/30);
}
void drawShape() {
x1 = (x1 + 1)%50 ; y1 = (y1+1)%50; x2 = x2 - 1; y2 = y2 -1 ;
if (x2 < 50) x2 = 100; if (y2 <50) y2 =100;
Rect r1 = new Rect(x1,y1, x2,y2);
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(5f);
paint.setColor(Color.YELLOW);
paint.setAlpha(alpha);
paint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.SOLID ));
canvas.drawOval(r1, paint)
}
}
If you look casually this code looks just fine. But look closely at the drawShape
function. As you have probably guessed, the problem here is we are creating Rect
and Paint
objects in every frame,i.e, whenever drawShape()
is being called by onDraw()
. This is firstly expensive on CPU to be allocating memory when it should actually be concerned with only drawing a shape, and add to that, with such frequent allocations to heap, your friend GC may just get interested. Now if, you look at the code closely, you can see how easily this problem can be fixed. All you need to do is to
class WrongView extends View {
.....
Rect r;
Paint paint; .....
public WrongView(Context context) {
super(context);
x1 = 0; y1 = 0; x2 = 100; y2 = 100;
alpha = 0;
animator = ValueAnimator.ofFloat(0f, 0.3f);
animator.addUpdateListener(animation -> {
float duration = (float)animation.getAnimatedValue();
alpha = (int) (256 * duration);
});
animator.start()
r = new Rect();
paint = new Paint();
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(5f);
paint.setColor(Color.YELLOW);
paint.setMaskFilter(new BlurMaskFilter(20, BlurMaskFilter.Blur.SOLID ));
}
.....
void drawShape() {
x1 = (x1 + 1)%50 ; y1 = (y1+1)%50; x2 = x2 - 1; y2 = y2 -1 ;
if (x2 < 50) x2 = 100; if (y2 <50) y2 =100;
r.set(x1,y1,x2,y2);
paint.setAlpha(alpha);
canvas.drawOval(r, paint)
}
}
Now, as you see we are using only 2 objects one for Rect
and one for Paint
. This helps you keep the allocation down and your animations should run smoother.
2. Being careless in using static objects
Objects unnecessarily marked static can cause memory hell. When you mark an object as static
, what you are telling the runtime is hold a reference to it till the class
where you created the static variable is unloaded. So essentially, this reference remains present the whole lifetime of application. So in nutshell, if you created a static reference to an object that object is doomed to be in heap forever because GC will never collect it. In some cases this is desired, but more than once I have seen some lazy developer decide that an object will be easier to access if defined static rather than passing it around. That's not how you should use static
. Let's see a very poor example, which I actually saw in code once.
....
public class MainActivity extends Activity {
public static Context context;
.....
@Override
protected void onCreate(Bundle savedInstanceState) {
MainActivity.context = this;
}
}
Now only purpose of this context
variable to be marked static was to make it easy to get context
where we want. As you know, you need the context
in doing a lot of stuff in Android and many libraries do need the context
, so the innocent developer thought rather than passing around the context everywhere in his utils
package, why not just mark it static. This is just wrong on many fronts but let's start with a memory perspective. If you look closely what he referred to as context
is actually the activity
object. Now, imagine this activity rendering high resolution images and keeping bitmaps and arrays as local variables in it. Now, since you marked this
as static all the memory that this Activity has allocated remains on heap forever. So now even after your activity has got onDestroy()
call the space allocated to Activity is still in memory. So, where is the surprise, if following activity launches are not performing well.
Since, we are on topic of context
, it is worth pointing out that you must understand difference between Application context
and Activity context
since they have different life cycles and have different information. Application context
follows lifecycle of the application and is better to mark static
as anyway this remains in memory for lifetime of Application. A lot of your 3rd party libraries and even Android APIs which require a context to be initialized or executed would be fine with Application context
and won't actually need activity context
, until obviously it needs some Activity specific information. So, before sending context its good to understand which one the API needs. Read this to understand more.
So in conclusion, next time before using the keyword static
, think a bit more. There are many good design patterns which would make it very easy to avoid it. Use static
only when there is an actual need.
3. Not understanding Dynamic arrays
Dynamic array implementation such as Vectors (C++) and ArrayList (Java) have made life a lot easier for developers and come in really handy when size of Array is not known beforehand. However, it is important to understand that this flexibility of size comes at a cost. When you initialise an ArrayList at a certain capacity, it allocates that much amount of memory for your List. Now, when you hit an overflow it has to grow in size, it has to allocate more memory, make a copy of current memory there and mark the old allocation to be deallocated. To break it down, let’s see pseudocode.
//Initial capacity is x
capacity = x ;
....
void add(T elem) {
if (overflow) {
new_capacity = x * GROW_FACTOR //GROW_FACTOR can be any number > 1 like 1.5 or 2
new_list = allocate(new_capacity)
copy(old_list, new_list)
old_list = new_list
capacity = new capacity
}
addElem(elem)
}
As you can see list growth is expensive on CPU (O(N) operation) and memory as well. Now, important thing for programmer to understand is that this is not to discourage you from stop using ArrayList or any such implementation of Dynamic list. What I am trying to say is we should care that we do not do too much growing operation in a short time, like say, having too many grow operation in one for loop. Like a lot of times even when we have capability to have a good estimate of final size of List we tend to not provide an initial capacity. Let’s see what I am trying to say by
List<Integer> getAllIds(Cursor cursor) {
List<Integer> result = new ArrayList<>();
while (cursor.moveToNext()) {
int id = cursor.getInt(cursor.getColumnIndex("id"));
result.add(id);
}
return result;
}
Now, at first glance this code looks just fine. But let’s see a bit more. Say, your cursor had 1000 elements. Since you did not assign a capacity it gets default inital capacity. For this example, we assume initial_capacity is 10 and GROW_FACTOR is 2. So, if we look at our pseudocode, we end up with 8 Grow operations in one loop. All this could have been avoided if we just made one small change
List<Integer> result = new ArrayList<>(cursor.getCount());
Also, it is important to use the right collection suited to your purpose and not default to ArrayList. For example, if you want linear access and random access it not urgent, use a LinkedList
in place of ArrayList
and so on.
4. Not Foreseeing Memory Leaks
Wait! Weren’t Memory leak a C thing. Shouldn’t GC ensure Memory can’t leak?
Well the answer is technically there can be no memory leak in Java until the runtime is able to recognise which objects are no longer needed. So basically a memory leak is a situation where the program no longer needs an object and thinks it has no reference to it but GC can still see references to it. There are many ways in which your Android app can leak memory and there are good tools to detect it. Leakcanary is an excellent library to detect such leaks. However, you must keep in mind that if leakcanary does not detect a memory leak it is not a guarantee that your app will not leak memory. You may have just not hit upon the situation in debug mode and the leak went undiscovered. So it is always good to understand how your app can leak memory. I have already told you about one of the ways in example I showed about misuse of static variables. In that case your activity
got leaked. Following are some of the other always
- Listeners and Observer Patterns
Consider this
class NetworkStatusReciever extends BroadcastReciever {
private static Set<NetworkCallback> networkCallbacks = new HashSet<>();
public void Register(NetworkCallback n) {
networkCallbacks.add(n)
}
}
....
public class MainActivity extends Activity implements NetworkCallback {
....
NetworkStatusReciever.Register(this);
}
Now, here you see activity has registered to receive events but it forgot to unregister. This will cause the Service to maintain a reference to the Activity and it will be ineligible for garbage collection and leak will occur.
- Anonymous or Inner classes Let’s see the example below
public class MainActivity extends Activity {
private TextView leaker;
private class LongTask extends AsyncTask<Void,Void,String> {
@Override
protected String doInBackground(Void... params) {
Thread.sleep(10000)
return "done";
}
@Override
protected void onPostExecute(String result) {
leaker.setText(result);
}
}
}
Here, all you need is your activity to be destroyed before your Thread.sleep
completes. How? Answer is, there is a reference to leaker
being held by the LongTask
which in turn ends up implicitly keeping a reference to entire Activity
. This could have been simply fixed by using WeakReference
, so you can edit your LongTask
a bit
...
private class LongTask extends AsyncTask<Void,Void,String> {
WeakReference<TextView> weakTextView;
public LongTask() {
weakTextView = new WeakReference<>(leaker);
}
@Override
protected String doInBackground(Void... params) {
Thread.sleep(10000)
return "done";
}
@Override
protected void onPostExecute(String result) {
if (weakTextView.get())
weakTextView.get().setText(result);
}
}
...
Similar problems can occur if you use Anonymous classes which hold implicit references to Activity.
There are other ways too in which your app can leak memory. You can read some more here. The point is, that it is important to understand that there are quite a few ways that memory can leak and they may be easily corrected by small changes to your code. However, detection may not always be easy, so, a basic understanding of how your Garbage Collection work will go a long way in identifying the problem before it happens.
5. Not doing due research before integrating a new 3rd party library
A good developer understands the purpose of not reinventing the wheel and reusing code and 3rd party libraries are great way to do it. But at same time, before you add any new library, it is desirable and at times very important to understand how that library works. I am not saying you read the entire Github repository but having some basic understanding does help, like how it stores your XMLs that it helps to parse, what kind of Data structures do they use to represent a graph, or even like whether they need app context or activity context to work is really good. Also, a little time spent evaluating the performance of library before you integrate can save you lots of pain that you may come across if it starts giving a problem once it is deep into your architecture.
Conclusion
An understanding of what goes beneath the hood while your runtime manages memory is a really good to have skill which will help you make high performance mobile applications. Remember, most Android phones have limited capabilities in terms of processing power and RAM. Not all your users will have a high end phone and they will appreciate your app much more if it gives a great experience even on low end devices. Also optimization of your app is not a one time task but should be an ongoing effort.
Reference and Further Reading
Originally published at gist.github.com.