Memory Management - Differences in C vs Java

Java does a lot of memory management for you, behind the scenes. C’s use of memory will seem clunky and weird by contrast. But there are advantages to being able to do your own memory management. For one thing, not having to rely on Java’s automated “garbage collection” can noticeably speed up your program. For another, there is less need to take the time to allocate throwaway data structures. We will discuss C pointers first, and then work into how memory management works, and its ramifications.

Pointers

In Java, a complex-type variable is really holding an address—a location in memory. When you pass an object to a method as an argument, you’re really just passing the address. That’s why changes to the object inside the method persist when it is done: the object inside the method is an alias of the one outside. For example, in Java you can do the following:

Arrays.sort(myArray); // Java's built-in sort function 

When this method is done, the array will have been changed—even though it has no return value. It used myArray’s address to effect all its changes.

In C, any variable can be handled this way, including basic variables like ints and doubles. The trick is to pass the variable’s address in memory, rather than the variable itself. That’s what a pointer is: it’s a variable that holds the address of another variable. (It “points to” the other variable.) To declare a pointer variable, we add a * before its name. (In this context, * has nothing to do with multiplication.) So to declare a pointer to an int, we use a line like the following:

int *myIntPointer; // can point to an int 

Two new operators become useful when we’re working with pointers: & and *. The & operator can be read as “address of”, and it returns the address of its variable. We can use it to set a pointer, as we do here:

myIntPointer = &value; // set pointer to be int's address 

The * operator can be read as “contents of”, and it returns the variable to which the pointer points. We can use it to print out the int that myIntPointer points to like this:

printf("%d\n", *myIntPointer); // prints pointed-to int 

If you actually wanted to know the value held in myIntPointer (not the value held by the int it points to), use the %p format specifier:

printf("%p\n", myIntPointer); // prints address (not int) 

We don’t use the * operator here, since we’re interested in the value of the pointer itself.

For every variable type, there’s a pointer type that can point to it. So there are int pointers (int*), char pointers (char*), etc. There are even pointers to pointers, such as the int** type that points to an int*. If you want to make a function that takes a pointer as an argument, use the * in its definition. This function takes a double pointer, and returns an int pointer:

int *doPointerThing(double *doublePointer) { 

That’s how functions like scanf() appear to modify their arguments. They aren’t really doing so—the actual arguments are addresses, and those can’t be changed by the function. But the values they point to are easily altered. This means that in C, some arguments are really functioning as extra return values in disguise.

Consider this function to swap the values of two int variables:

// exchange 2 ints\                                                
void swap(int *value1, int *value2) {                         
int temp = *value1; // temp <- contents of 1st pointer *value1  
= *value2; // contents of 1st <- contents of 2nd *value2 =      
temp; // contents of 2nd <- temp                                  
                                                                   
}                                                                

This void function technically doesn’t return anything. Instead, it just takes two int pointers, and exchanges their contents. To call the function, just do this:

swap(&x, &y); // x and y are just ints! 

After it’s done, x and y will have switched values.

This style of coding is extremely common in C, but it can’t be done in Java. It lets a programmer return multiple values, or deal with primitive variables like they’re complex types. It’s efficient, because there’s no need to mess with arrays or complicated temporary data structures. By taking in the addresses of the ints, everything becomes streamlined. However it does require care and understanding that are not needed in Java.

Finally, the void pointer is a generic pointer, that can point at anything:

void *anythingPointer; // not limited to one type 

This kind of pointer can be especially useful as a return value, when a function needs to effectively return any kind of pointer.

You might be surprised that when we declare a pointer, we attach the * to the name of the variable, and not the type. The following code might look more intuitive to a Java programmer:

int* myIntPointer; // not standard C 

The reason we don’t do this is because every single pointer variable must have its own *. If you were to do this:

int* pointer1, pointer2; // bug: pointer2 is an int 

The compiler would associate the * with pointer1, while pointer2 would mistakenly be just an int. The proper way to code this is as follows:

int *pointer1, *pointer2; // both are int pointers 

Figure 1: The stack and the heap. The stack is used to store separate data for each function. Here, main() has called func1(), which has called func2(), and each needs its own space for local variables and the like. Meanwhile the heap is a general-use space for allocation. Here, seven different blocks are currently checked out from the operating system.

The Heap & the Stack

Before we continue, we need to explain the heap and the stack. These are the areas of memory on a computer that we allocate to store variables and data structures.

First of all, in both C and Java we need memory every time we call a function. We need this memory for:

  • all the local variables and arguments

  • the address of the previous function, to return to upon completion

  • other miscellaneous overhead

A function stores all this data in a structure called a frame, and each currently running function has its own frame. Each time a function is called, a new frame is created for it. When a function ends, its frame isn’t needed anymore and it can be deleted. If a function is called recursively, the new incarnation of the function needs its own local variables and return address that are distinct from those of the old version. Therefore each call gets its own frame.

All of the frames are stored on a stack data structure, as shown in Figure 1. Properly, this stack is called the execution stack, but it is so important to a program that it is usually just called the stack. The top frame will always be the frame of the currently running function; it is called the active frame. When a program first starts up, the only frame is that of the main() function. But when a new function is called, its frame gets created and pushed onto the top of the stack, becoming the new active frame. When a function ends, its frame is removed and the one beneath it is once more the active frame. Eventually the main() function’s frame will be the only one left, and when it terminates the stack is empty, and the program is done.

By contrast, a program’s heap is organized more as a “lending library”of memory, as shown in Figure 1. Regions of contiguous memory can be”checked out” for the program’s use. It is managed by the computer’s operating system (OS), which has ultimate control over how much memory each active program gets. When a program needs memory from the heap, it uses a system call to flag the OS and request how much memory it needs. Since the OS is probably busy with other programs, this program must pause while it waits for an answer. Eventually the OS will be able to check if it has enough available memory. If so, it will likely approve the request, returning the address where the newly checked-out memory is located. But it may also deny the request and return a null pointer instead. If this happens, it will probably result in the program not being able to do its task, causing it to shut down. But assuming the request was approved, the program may use the memory for any needed data structure for as long as it wishes.

Note: The heap we’re talking about here is not at all related to the heap data structure that is used by heapsort and priority queues. Don’t confuse them.

When a program is done with memory that was checked out, it is expected to free the memory—returning it to the heap. In C this must be done explicitly, or else it will never be returned to the heap for future use. This condition is called a memory leak, and it is a serious bug. If one program continues to request memory without giving any back, it will slowly eat up more and more of the computer’s resources, until the computer slows to a crawl (because it is using virtual memory), and eventually other programs begin to fail due their memory requests being denied.

In general, data stored on the heap is more permanent. However, it takes more management, and allocating it can be slower because it requires the attention of the OS. By contrast, allocating data from the stack is more temporary. But it can be very fast because no system calls need to be made. Keeping this in mind, there are two major differences between memory management in Java and C:

  1. In Java, local variables are always allocated from the stack, while arrays and objects are always allocated from the heap. But in C, you can often choose whether to allocate from the stack or the heap.

  2. In Java, returning memory to the heap is done automatically by the Java garbage collector. This finds objects that aren’t being used anymore, and automatically frees them. But in C, you must explicitly free memory that you previously allocated (or else have a memory leak).

Allocating Arrays

The array declaration techniques you already know allocate memory from the stack:

int myArray[20]; // array of 20 ints on the stack 

This has the problem mentioned above: the array is inherently linked to the function in which it is declared. When the function ends, so does the array. Soon, a new frame will be created, which overwrites the array.

There’s another problem here that is less obvious: an array stored on the stack is stored very close to the function’s return address. A malicious hacker might be able to trick your program into overwriting the return address, and executing hostile code!Thus, you should be very careful when indexing arrays with you allocated from the stack.

To make a permanent array from the heap, you must use the malloc() function, and store the result into a pointer variable. The name “malloc”means “memory allocation”, and it is roughly equivalent to Java’s new keyword. It takes one argument: the size of the array in bytes, and if successful it returns a pointer to the allocated memory. Here is an example:

  int *myArray = (int*)malloc(20 * sizeof(int)); // from heap

This requests space for 20 ints: on most computers sizeof(int) will be 4, and so this requests 80 bytes. Since malloc() returns a generic void, you must cast this into an int. You can permanently allocate a string in a similar way:

// allocate space for a length-30 string (plus 0 at end) char      
*someString = (char*)malloc(31 * sizeof(char));                 

In C, arrays and pointers are almost interchangeable. After all, what is an array, but the address of a series of contiguous elements? Because the elements are contiguous, the address of the array is also the address of the first element, element 0. But that’s what a pointer is: the address of an element. So when we cast the results of a malloc() as, for example, an int pointer, we can treat it just like an int array. Look at this code:

This is the big problem with the gets() function, which you should never, ever use. It takes an input string from the user but cannot control how big the input is, making it very easy for an intelligent adversary to overwrite data and hijack your program.

int *myArray = (int*)malloc(20 * sizeof(int)); // allocate      
*(myArray + 9) = 13; // set element 9 to 13                      
myArray[9] = 13; // set element 9 to 13 (same thing)             

The second and third lines do the exact same thing: they take myArray[9] (which is 36 bytes away from myArray[0] on a system with 4-byte ints), and set it to 13. It is very common practice to use malloc() to allocate a pointer, but from that point on use it as an array.

This last detail illustrates an important detail about pointer arithmetic. You may add or subtract an int from a pointer. But when you do so, the int is first multiplied by the size of the type that the pointer points to. That is why in the above case adding 9 to an address resulted in an address 36 bytes away. This helps you to treat pointers like arrays. Further, if you subtract one pointer from another, the result is divided by the type size. So myArray[9] - myArray[0] is in fact 9, even though the two addresses are 36 bytes apart.

Remember, when your program calls malloc(), it only requests memory from the heap. This request is never guaranteed: the operating system can always decide to deny it (usually when it cannot find enough space). If the request gets denied, malloc() will return a NULL pointer, which is address 0. Thus, it is usually prudent to test that the program gets a valid address back. That way if there’s a problem, the program can terminate in a controlled manner:

if (!myArray) {                                     
// allocation was denied; time to shut down gracefully 
                                                       
}                                                    

(Remember that ! works on ints in C, since there are no booleans. So the code if (!myArray) is equivalent to the code if (myArray == 0).) In Java, the garbage collector takes over your program periodically. It scans your objects and arrays, finding ones that are no longer being used and returning their memory to the heap, automatically. This makes the program less efficient, but it does make development much easier. However, C does not do this for you. Rather, you are expected to know when memory from the heap is no longer needed, and to return it explicitly. This is done via the free() function:

free(myArray); // give myArray back to the heap 

If you do not free memory properly, you will have a memory leak bug—with all the bad effects detailed above. Every call to malloc() must have a corresponding call to free().

To allocate a two-dimensional array, you need a pointer to a pointer. For example, the following code will allocate a 4 x 5 int array from the heap:

// start by mallocing the array of arrays                        
int **array = (int**)malloc(4 * sizeof(int*));\             
for (int i=0; i<4; i++) { // now for each subarray...          
   array[i] = (int*)malloc(5 * sizeof(int)); // ...malloc                                                                       
}                                                                

A three-dimensional pointer would require a ***int type, and so on. Also remember that a string is already a char array. So if you want a string array, it must really be a 2D char array. This is why the main() argument argv, which holds an array of the command-line arguments, needs to be of type char**.

Segmentation Faults

One of the most common errors you will get in a C program is a segmentation fault. A segment is a region of memory on your computer, that your operating system allocates out to different programs. Both the heap and the stack are made of segments. However, the OS will not permit programs to access the wrong segment. This is for both protection and security: one rogue program shouldn’t be able to interfere with the others or to bring the whole system down. Thus, whenever you try to access memory outside of your allotted region, the OS will forcibly halt your program. This is what a segmentation fault is.

These “seg faults” can be particularly frustrating, because your program often won’t give you any clues as to what caused it. (Unlike Java, that will usually give you a helpful error message when it crashes.) Here are some of the most common causes behind seg faults:

  • You tried to use a null pointer, or a pointer that hasn’t been properly set yet. (This would usually be a NullPointerException in Java.)

  • You tried to access an array beyond its bounds, reaching into a forbidden area of memory. (This would be an ArrayIndexOutOfBoundsException in Java.)

  • You requested too much memory in a malloc(), and it returned a null pointer that you followed. (This would be an OutOfMemoryError in Java.

  • Your recursive function could not stop and kept recursing, running out of stack space. (This would be a StackOverflowError in Java.)

  • You accidentally overwrote the return address in a frame, and when the function ended you tried to follow it back to a forbidden address. (This cannot happen in Java.)

  • You tried to use a pointer that has already been freed. (This cannot happen in Java.)

  • You accidentally overwrote a pointer variable, and then tried to follow it to a forbidden address. (This cannot happen in Java.)

  • You tried to use an array stored in the stack, after its function finished and its frame was overwritten. (This cannot happen in Java.)

Remember that your bug might not cause a crash immediately. It could corrupt your data in some way that only causes a segmentation fault later on.


Licenses and Attributions


Speak Your Mind

-->