Memory Management in Java Interview Questions and Answers

Q: What does the statement “memory is managed in Java” mean?
Memory is the key resource an application requires to run effectively and like any resource, it is scarce. As such, its allocation and deallocation to and from applications or different parts of an application require a lot of care and consideration.

However, in Java, a developer does not need to explicitly allocate and deallocate memory – the JVM and more specifically the Garbage Collector – has the duty of handling memory allocation so that the developer doesn’t have to.

This is contrary to what happens in languages like C where a programmer has direct access to memory and literally references memory cells in his code, creating a lot of room for memory leaks.

Q: What is Garbage Collection and what are its advantages?
Garbage collection is the process of looking at heap memory, identifying which objects are in use and which are not, and deleting the unused objects.

An in-use object, or a referenced object, means that some part of your program still maintains a pointer to that object. An unused object, or unreferenced object, is no longer referenced by any part of your program. So the memory used by an unreferenced object can be reclaimed.

The biggest advantage of garbage collection is that it removes the burden of manual memory allocation/deallocation from us so that we can focus on solving the problem at hand.

Q: Are there any disadvantages of Garbage Collection?
Yes. Whenever the garbage collector runs, it has an effect on the application’s performance. This is because all other threads in the application have to be stopped to allow the garbage collector thread to effectively do its work.

Depending on the requirements of the application, this can be a real problem that is unacceptable by the client. However, this problem can be greatly reduced or even eliminated through skillful optimization and garbage collector tuning and using different GC algorithms.

Q: What is the meaning of the term “stop-the-world”?

When the garbage collector thread is running, other threads are stopped, meaning the application is stopped momentarily. This is analogous to house cleaning or fumigation where occupants are denied access until the process is complete.

Depending on the needs of an application, “stop the world” garbage collection can cause an unacceptable freeze. This is why it is important to do garbage collector tuning and JVM optimization so that the freeze encountered is at least acceptable.

Q: What are stack and heap? What is stored in each of these memory structures, and how are they interrelated?
The stack is a part of memory that contains information about nested method calls down to the current position in the program. It also contains all local variables and references to objects on the heap defined in currently executing methods.

This structure allows the runtime to return from the method knowing the address whence it was called, and also clear all local variables after exiting the method. Every thread has its own stack.

The heap is a large bulk of memory intended for allocation of objects. When you create an object with the new keyword, it gets allocated on the heap. However, the reference to this object lives on the stack.

Q: What is generational garbage collection and what makes it a popular garbage collection approach?
Generational garbage collection can be loosely defined as the strategy used by the garbage collector where the heap is divided into a number of sections called generations, each of which will hold objects according to their “age” on the heap.

Whenever the garbage collector is running, the first step in the process is called marking. This is where the garbage collector identifies which pieces of memory are in use and which are not. This can be a very time-consuming process if all objects in a system must be scanned.

As more and more objects are allocated, the list of objects grows and grows leading to longer and longer garbage collection time. However, empirical analysis of applications has shown that most objects are short-lived.

With generational garbage collection, objects are grouped according to their “age” in terms of how many garbage collection cycles they have survived. This way, the bulk of the work spread across various minor and major collection cycles.

Today, almost all garbage collectors are generational. This strategy is so popular because, over time, it has proven to be the optimal solution.

Q: Describe in detail how generational garbage collection works
To properly understand how generational garbage collection works, it is important to first remember how Java heap is structured to facilitate generational garbage collection.

The heap is divided up into smaller spaces or generations. These spaces are Young Generation, Old or Tenured Generation, and Permanent Generation.

The young generation hosts most of the newly created objects. An empirical study of most applications shows that majority of objects are quickly short lived and therefore, soon become eligible for collection. Therefore, new objects start their journey here and are only “promoted” to the old generation space after they have attained a certain “age”.

The term “age” in generational garbage collection refers to the number of collection cycles the object has survived.

The young generation space is further divided into three spaces: an Eden space and two survivor spaces such as Survivor 1 (s1) and Survivor 2 (s2).

The old generation hosts objects that have lived in memory longer than a certain “age”. The objects that survived garbage collection from the young generation are promoted to this space. It is generally larger than the young generation. As it is bigger in size, the garbage collection is more expensive and occurs less frequently than in the young generation.

The permanent generation or more commonly called, PermGen, contains metadata required by the JVM to describe the classes and methods used in the application. It also contains the string pool for storing interned strings. It is populated by the JVM at runtime based on classes in use by the application. In addition, platform library classes and methods may be stored here.

First, any new objects are allocated to the Eden space. Both survivor spaces start out empty. When the Eden space fills up, a minor garbage collection is triggered. Referenced objects are moved to the first survivor space. Unreferenced objects are deleted.

During the next minor GC, the same thing happens to the Eden space. Unreferenced objects are deleted and referenced objects are moved to a survivor space. However, in this case, they are moved to the second survivor space (S1).

In addition, objects from the last minor GC in the first survivor space (S0) have their age incremented and are moved to S1. Once all surviving objects have been moved to S1, both S0 and Eden space are cleared. At this point, S1 contains objects with different ages.

At the next minor GC, the same process is repeated. However this time the survivor spaces switch. Referenced objects are moved to S0 from both Eden and S1. Surviving objects are aged. Eden and S1 are cleared.

After every minor garbage collection cycle, the age of each object is checked. Those that have reached a certain arbitrary age, for example, 8, are promoted from the young generation to the old or tenured generation. For all subsequent minor GC cycles, objects will continue to be promoted to the old generation space.

This pretty much exhausts the process of garbage collection in the young generation. Eventually, a major garbage collection will be performed on the old generation which cleans up and compacts that space. For each major GC, there are several minor GCs.

Q: When does an object become eligible for garbage collection? Describe how the GC collects an eligible object?

An object becomes eligible for Garbage collection or GC if it is not reachable from any live threads or by any static references.

The most straightforward case of an object becoming eligible for garbage collection is if all its references are null. Cyclic dependencies without any live external reference are also eligible for GC. So if object A references object B and object B references Object A and they don’t have any other live reference then both Objects A and B will be eligible for Garbage collection.

Another obvious case is when a parent object is set to null. When a kitchen object internally references a fridge object and a sink object, and the kitchen object is set to null, both fridge and sink will become eligible for garbage collection alongside their parent, kitchen.

Q: How do you trigger garbage collection from Java code?

You, as Java programmer, can not force garbage collection in Java; it will only trigger if JVM thinks it needs a garbage collection based on Java heap size.

Before removing an object from memory garbage collection thread invokes finalize()method of that object and gives an opportunity to perform any sort of cleanup required. You can also invoke this method of an object code, however, there is no guarantee that garbage collection will occur when you call this method.

Additionally, there are methods like System.gc() and Runtime.gc() which is used to send request of Garbage collection to JVM but it’s not guaranteed that garbage collection will happen.

Q: What happens when there is not enough heap space to accommodate storage of new objects?

If there is no memory space for creating a new object in Heap, Java Virtual Machine throws OutOfMemoryError or more specifically java.lang.OutOfMemoryError heap space.

Q: Is it possible to «resurrect» an object that became eligible for garbage collection?
When an object becomes eligible for garbage collection, the GC has to run the finalize method on it. The finalize method is guaranteed to run only once, thus the GC flags the object as finalized and gives it a rest until the next cycle.

In the finalize method you can technically “resurrect” an object, for example, by assigning it to a static field. The object would become alive again and non-eligible for garbage collection, so the GC would not collect it during the next cycle.

The object, however, would be marked as finalized, so when it would become eligible again, the finalize method would not be called. In essence, you can turn this “resurrection” trick only once for the lifetime of the object. Beware that this ugly hack should be used only if you really know what you’re doing — however, understanding this trick gives some insight into how the GC works.

Q: How does Java allocate stack and heap memory?
Each time an object is created in Java it goes into the area of memory known as heap. The primitive variables like int and double are allocated in the stack (i.e. Last In First Out queue), if they are local variables and in the heap if they are member variables (i.e. fields of a class). In Java methods and local variables are pushed into stack when a method is invoked and stack pointer is decremented when a method call is completed. In a multi-threaded application each thread will have its own stack but will share the same heap. This is why care should be taken in your code to avoid any concurrent access issues in the heap space. The stack is thread-safe because each thread will have its own stack.

Q: Is there any possible memory leak in Java?
Memory leak is unused but referenced part of the memory. Though GC takes care of cleaning up your memory/heap, but if you have used some native objects and forgot to reclaim the memory explicitly because that’s anyway not going to be taken care by the GC (which takes care of heap memory management only).

Similarly, using ‘static’ also can be one of the potential reasons for memory leaks in Java. ‘static’ can’t straightway be blamed for causing memory leaks; but if programmer has not taken care of the setting the references to ‘null’ explicitly after using the static objects then they can definitely cause memory leaks. Since ‘static’ members will by default live for the entire life of an app unless they are explicitly set to ‘null’. So, always make it a point to nullify the references as soon as you reach at a point in your code where the use of the static member is over.

Ex: Suppose you have created a ‘Statement’ object from a DB Connection and the connection is a pooled one. Now as you know calling close() method on a pooled connection will not actually close the connection instead it will return the Connection object to the pool to be re-used.

Memory Pool Examples:

Allocate a block of memory in a pool


my_object *obj;
res = mps_alloc((mps_addr_t *)&obj, pool, sizeof *obj);
if (res != MPS_RES_OK)
    error(...);

Allocation point protocol in C


mps_addr_t p;
obj_t obj;
size_t aligned_size = ALIGN(size); /* see note 1 */
do {
    mps_res_t res = mps_reserve(&p, ap, aligned_size);
    if (res != MPS_RES_OK)
        /* handle the error */;
    /* p is now an ambiguous reference to the reserved block */
    obj = p;
    /* initialize obj */
} while (!mps_commit(ap, p, aligned_size)); /* see note 2 */
/* obj is now valid and managed by the MPS */

So, in such a case unless you explicitly close the ‘Statement’ object, it would keep consuming precious memory space for no real use. Now if you have declared the ‘Statement’ object as a static member, it’ll be maintained in the memory for the entire life time of the app even when the control is out of the scope. Now that if your Statement object is non-static, it will eligible for garbage collection, once it is out-of-scope. However, there is still wastage of memory after using the Statement last and before reaching the end of the local scope.

Therefore, in summary we can say that one should/must :-
Always think if you really need to make this variable/member a ‘static’.
Always try to confine the scope of an object to restrict its usage only to the section it’s actually needed.
Always make a conscious effort to explicitly nullify objects once you finish using them (especially the large objects)

Q: Explain memory management in java.
In java, memory is managed via garbage collector. Few techniques for memory management are:

1. Reference Counting: A count of references to each object is maintained. When garbage collector runs, it deletes objects with zero reference count.
Drawback: Circular references are maintained in memory.

2. Tracing collectors/Copy Collector/Stop and copy collector: Start from a root object and keep track of all references which have direct or indirect reference to the root object. Then all the live objects are moved to another heap, taking care of references properly.
Drawback: At each point of time, you will have 2 heaps thus consuming twice the memory.

3. Mark sweep collectors/Stop and work collector: Similar to tracing collector except that instead of copying the references to the new heap, they are swept out of memory, after a list of live and dead objects is known.

Mark and sweep is a stop-the-world garbage collection technique; that is all application threads stop until garbage collection completes or until a higher-priority thread interrupts the garbage collector. If the garbage collector is interrupted it must restart which can lead to application thrashing with little apparent result.

http://www.ibm.com/developerworks/java/library/j-jtp10283/

Q: Does Java have destructors?
Garbage collector does the job working in the background. Java does not have destructors; but it has finalizer that does a similar job.
Syntax is: public void finalize() { }

If an object has a finalizer, the method is invoked before the system garbage collects the object.

Q: Does the finalize method in subclass invoke finalize method in super class?
Finalize is not implicitly chained. A finalize method in sub-class should call finalize in super class explicitly as its last action for proper functioning. Compilers don’t enforce this check.

Q: Can finalize method be overloaded?
Yes but only the following version is called by garbage collector:

protected void finalize() throws Throwable { };

Q: Should I override finalize method?
Unlike C++ destructors, the finalize() method in Java is unpredictable, often dangerous and generally unnecessary. Use try and finally blocks while implementing finalize method. The finalize() method should only be used in rare instances as a safety net or to terminate non-critical native resources. If you do happen to call the finalize() method in some rare instances then remember to call the super.finalize() as shown below:

protected void finalize() throws Throwable {
try {
//finalize subclass state
}

finally {
super.finalize();
}
}

Q: An object is resurrected by making other object refer to the dying object in finalize method. Will this object be ever garbage collected?
Resurrection can happen in finalize method which will prevent GC to reclaim the object memory. However this could be done only once. Next time GC will not invoke finalize method before garbage collection.

Well, the problem is that an object which overrides finalize() must now be determined to be garbage in at least two separate garbage collection cycles in order to be collected. When the first cycle determines that it is garbage, it becomes eligible for finalization. Because of the (slim, but unfortunately real) possibility that the object was “resurrected” during finalization, the garbage collector has to run again before the object can actually be removed. And because finalization might not have happened in a timely fashion, an arbitrary number of garbage collection cycles might have happened while the object was waiting for finalization. This can mean serious delays in actually cleaning up garbage objects, and is why you can get OutOfMemoryErrors even when most of the heap is garbage.

Q: Explain types of references in Java? java.lang.ref package can be used to declare soft, weak and phantom references.
There are actually four different degrees of reference strength: strong, soft, weak, and phantom, in order from strongest to weakest:

Strong Reference: By default.

Weak references: A weak reference is a reference that isn’t strong enough to force an object to remain in memory. Weak references allow you to leverage the garbage collector’s ability to determine reachability for you, so you don’t have to do it yourself. You create a weak reference like this:

WeakReference weakWidget = new WeakReference(widget);
weakWidget.get() // get the actual Widget

Of course the weak reference isn’t strong enough to prevent garbage collection, so you may find (if there are no strong references to the widget) that weakWidget.get() suddenly starts returning null.

Soft references: A soft reference is exactly like a weak reference, except that it is less eager to throw away the object to which it refers. An object which is only weakly reachable (the strongest references to it are WeakReferences) will be discarded at the next garbage collection cycle, but an object which is softly reachable will generally stick around for a while. Soft References aren’t required to behave any differently than WeakReferences, but in practice softly reachable objects are generally retained as long as memory is in plentiful supply. This makes them an excellent foundation for a cache, since you can let the garbage collector worry about both how reachable the objects are and how badly it needs the memory they are consuming.

Phantom references
A phantom reference is quite different than either SoftReference or WeakReference. Its grip on its object is so tenuous that you can’t even retrieve the object — its get() method always returns null. The only use for such a reference is keeping track of when it gets enqueued into a ReferenceQueue, as at that point you know the object to which it pointed is dead. How is that different from WeakReference, though?

The difference is in exactly when the enqueuing happens. WeakReferences are enqueued as soon as the object to which they point becomes weakly reachable. This is before finalization or garbage collection has actually happened. In case of Weak Reference, object could even be “resurrected” by an finalize() method, but the WeakReference would remain dead. PhantomReferences are enqueued only when the object is physically removed from memory, and the get() method always returns null specifically to prevent you from being able to “resurrect” an almost-dead object.

Use of Phantom Reference:
1. They allow you to determine exactly when an object was removed from memory. They are in fact the only way to determine that. This isn’t generally that useful, but might come in handy in certain very specific circumstances like manipulating large images: if you know for sure that an image should be garbage collected, you can wait until it actually is before attempting to load the next image, and therefore make the dreaded OutOfMemoryError less likely.

2. PhantomReferences avoid a fundamental problem with finalization – resurrection. With PhantomReference, resurrection is impossible. When a PhantomReference is enqueued, there is absolutely no way to get a pointer to the now-dead object.Arguably, the finalize() method should never have been provided in the first place. PhantomReferences are definitely safer and more efficient to use, and eliminating finalize() would have made parts of the VM considerably simpler. But, they’re also more work to implement, so I confess to still using finalize() most of the time. The good news is that at least you have a choice.

Q: Talk about garbage collector for various references.
If an element is determined to be eligible for processing, GC must determine if it is eligible for collection. The first criterion here is simple. Is the referent marked? If it is marked, the reference object is not eligible for collection and GC moves onto the next element of the list. However, if the referent is not marked and is eligible for collection, the process differs for each reference type.

Soft references are collected if their referent has not been marked for the previous 32 garbage collection cycles. You adjust the frequency of collection with the -Xsoftrefthreshold option. If there is a shortage of available storage, all soft references are cleared. All soft references are guaranteed to have been cleared before the OutOfMemoryError is thrown.

Weak and phantom references are always collected if their referent is not marked.

Q: Explain garbage collection on Remote Objects or Distributed Garbage collection.
In a distributed system, just as in the local system, it is desirable to automatically delete those remote objects that are no longer referenced by any client. This frees the programmer from needing to keep track of the remote objects’ clients so that it can terminate appropriately. RMI uses a reference-counting garbage collection algorithm for the same.

To accomplish reference-counting garbage collection, the RMI runtime keeps track of all live references within each Java virtual machine. When a live reference enters a Java virtual machine for first time, it sends a “referenced” message to the server for the object. Going forward, whenever a live reference enters JVM, its reference count is incremented and is decremented as soon as it leaves the JVM. When the last reference has been discarded, an unreferenced message is sent to the server. Many subtleties exist in the protocol; most of these are related to maintaining the ordering of referenced and unreferenced messages in order to ensure that the object is not prematurely collected.

When a remote object is not referenced by any client, the RMI runtime refers to it using a weak reference. The weak reference allows the Java virtual machine’s garbage collector to discard the object if no other local references to the object exist. As long as a local reference to a remote object exists, it cannot be garbage-collected and it can be passed in remote calls or returned to clients. Remote objects are only collected when no more references, either local or remote, still exist. The distributed garbage collection algorithm interacts with the local Java virtual machine’s garbage collector in the usual ways by holding normal or weak references to objects.

In addition to the reference counting mechanism, a live client reference has a lease with a specified time. When the client is done with the reference and allows the remote stub to go out of scope, or when the lease on the object expires, the reference layer on the host automatically deletes the record of the remote reference and notifies the client’s reference layer that this remote reference has expired. The lease time is controlled by the system property java.rmi.dgc.leaseValue. The value is in milliseconds and defaults to 10 minutes. The concept of expirable leases, as opposed to strict on/off references, is used to deal with situations where a client-side failure or a network failure keeps the client from notifying the server that it is done with its reference to an object.

A remote object needing unreferenced notification must implement the java.rmi.server.Unreferenced interface. When those references no longer exist, the unreferenced method will be invoked.

Q: Does OutOfMemoryError and StackOverFlowError cause JVM crash?
Any problem in PURE Java code throws a Java exception or error. Java exceptions or errors will NOT cause a core dump (on UNIX systems) or a Dr.Watson error (on WIN32systems). Any serious Java problem will result in an OutOfMemoryError thrown by the JVM with the stack trace and consequently JVM will exit. An OutOfMemoryError (not jvm crash) can be thrown due to one of the following 4 reasons:

1. JVM may have a memory leak due to a bug in its internal heap management implementation. But this is highly unlikely because JVMs are well tested for this.

2. The application may not have enough heap memory allocated for its running. You can allocate more JVM heap size (with –Xmx parameter to the JVM) or decrease the amount of memory your application takes to overcome this. You can increase heap size as below:
java -Xms1024M -Xmx1024M

Care should be taken not to make the –Xmx value too large because it can slow down your application.

3. Another not so prevalent cause is the running out of a memory area called the “perm” which sits next to the heap. All the binary code of currently running classes is archived in the “perm” area. The ‘perm’ area is important if your application or any of the third party jar files you use dynamically generate classes.
For example: “perm” space is consumed when XSLT templates are dynamically compiled into classes, J2EE application servers, JasperReports, JAXB etc use Java reflection to dynamically generate classes and/or large amount of classes in your application. To increase perm space:
java -XX:PermSize=256M -XX:MaxPermSize=256M

4. The fourth and the most common reason is that you may have a memory leak in your application.

Q: Different OutOfMemory errors.
Let’s have a look at the Sun HotSpot JVM and its concrete implementation of OutOfMemoryError errors.

1. In the heap we get an OutOfMemoryError, if the garbage collector cannot reclaim enough memory for a new object. In such situation the Sun HotSpot JVM shows this error message:
java.lang.OutOfMemoryError: Java heap space

2. An alternative for this is as below, it occurs when application tries to create an array on the heap that is bigger than the total heap size.
java.lang.OutOfMemoryError: Requested array size exceeds VM limit

3. If there is not enough memory in the method area for creating a new class, the Sun HotSpot implementation gets an error in the permanent generation:
java.lang.OutOfMemoryError: PermGen space

4. OutOfMemory errors in thread exclusive memory areas occur less frequently and are identified by the following error messages in the Sun HotSpot JVM:
java.lang.OutOfMemoryError: unable to create new native thread

This occurs if there are too many threads in the JVM and there is not enough memory left to create a new thread. I’ve seen this because the memory limits of a process have been reached (especially in 32bit operating systems, e.g. on Windows 32bit it is 2GB) or the maximum number of file handles for the user that executes the java process has been reached.

5. It indicates that a memory allocation error on a native stack (JNI method call) has occured.
java.lang.OutOfMemoryError: (Native method)

6. It is also interesting that a memory allocation error on the JVM stack (too many frames on the stack) does not throw an Java OutOfMemory error but as the JVM specification mandates.
java.lang.StackOverflowError

7. The last variant of the OutOfMemoryError is out of swap space. This error is thrown if there is not enough memory left on the operating system level – which is normally true if other processes are using all of the available memory or the swap space is configured too small.
java.lang.OutOfMemoryError: request bytes for .

Q: Why does the JVM crash with a core dump or a Dr.Watson error?
Both the core dump on UNIX operating system and Dr.Watson error on WIN32 systems mean the same thing. If you define a crash as an unhandled problem (i.e. no Java Exception or Error); then this cannot be done from within Java. The JVM is a process like any other and when a process crashes a core dump is created. A core dump is a memory map of a running process. This can happen due to one of the following reasons:

1. Using JNI (Java Native Interface) code containing a fatal bug in it. Typical crashes in native code happen by dereferencing pointers to wrong memory areas (like Nullpointer) or illegal opcodes.
For ex: using Oracle OCI drivers, which are written partially in native code or JDBC-ODBC bridge drivers, which are written in non Java code. Using 100% pure Java drivers (communicates directly with the database instead of through client software utilizing the JNI) instead of native drivers can solve this problem.

2. The OS on which your JVM is running might require a patch or service pack.

3. The JVM implementation may have a bug in translating system resources like threads, file handles, sockets etc from the platform neutral Java byte code into platform specific operations. If this JVM’s translated native code performs an illegal operation then the operating system will instantly kill the process and mostly will generate a core dump file.

The core dump files are generated by the operating system in response to certain signals. The JVM can also intercept certain signals like SIGQUIT which is kill -3 from the operating system and it responds to this signal by printing out a Java stack trace and then continue to run. On the other hand signals like SIGSTOP (kill -23 ) and SIGKILL (kill -9 ) will cause the JVM process to stop or die. The JVM argument “java –Xsqnopause” will indicate JVM not to pause on SIGQUIT signal from OS.

4. On Linux/Unix, it is easy to crash JVM crash by sending it a Signal to the running process.

Note: You should not use “SIGSEGV” for this, since JVM catches this signal and re-throws it as a NullPointerException in most places. So it is better to send a SIGBUS.

Q: Describe strong, weak, soft and phantom references and their role in garbage collection.

Much as memory is managed in Java, an engineer may need to perform as much optimization as possible to minimize latency and maximize throughput, in critical applications. Much as it is impossible to explicitly control when garbage collection is triggered in the JVM, it is possible to influence how it occurs as regards the objects we have created.

Java provides us with reference objects to control the relationship between the objects we create and the garbage collector.

By default, every object we create in a Java program is strongly referenced by a variable:

1
StringBuilder sb = new StringBuilder();

In the above snippet, the new keyword creates a new StringBuilder object and stores it on the heap. The variable sb then stores a strong reference to this object. What this means for the garbage collector is that the particular StringBuilder object is not eligible for collection at all due to a strong reference held to it by sb. The story only changes when we nullify sb like this:

1
sb = null;

After calling the above line, the object will then be eligible for collection.

We can change this relationship between the object and the garbage collector by explicitly wrapping it inside another reference object which is located inside java.lang.ref package.

A soft reference can be created to the above object like this:

1
2
3
StringBuilder sb = new StringBuilder();
SoftReference<StringBuilder> sbRef = new SoftReference<>(sb);
sb = null;

In the above snippet, we have created two references to the StringBuilder object. The first line creates a strong reference sb and the second line creates a soft reference sbRef. The third line should make the object eligible for collection but the garbage collector will postpone collecting it because of sbRef.

The story will only change when memory becomes tight and the JVM is on the brink of throwing an OutOfMemory error. In other words, objects with only soft references are collected as a last resort to recover memory.

A weak reference can be created in a similar manner using WeakReference class. When sb is set to null and the StringBuilder object only has a weak reference, the JVM’s garbage collector will have absolutely no compromise and immediately collect the object at the very next cycle.

A phantom reference is similar to a weak reference and an object with only phantom references will be collected without waiting. However, phantom references are enqueued as soon as their objects are collected. We can poll the reference queue to know exactly when the object was collected.

Q: Suppose we have a circular reference (two objects that reference each other). Could such pair of objects become eligible for garbage collection and why?

Yes, a pair of objects with a circular reference can become eligible for garbage collection. This is because of how Java’s garbage collector handles circular references. It considers objects live not when they have any reference to them, but when they are reachable by navigating the object graph starting from some garbage collection root (a local variable of a live thread or a static field). If a pair of objects with a circular reference is not reachable from any root, it is considered eligible for garbage collection.

Q: How are strings represented in memory?

A String instance in Java is an object with two fields: a char[] value field and an int hash field. The value field is an array of chars representing the string itself, and the hash field contains the hashCode of a string which is initialized with zero, calculated during the first hashCode() call and cached ever since. As a curious edge case, if a hashCode of a string has a zero value, it has to be recalculated each time the hashCode() is called.

Important thing is that a String instance is immutable: you can’t get or modify the underlying char[] array. Another feature of strings is that the static constant strings are loaded and cached in a string pool. If you have multiple identical String objects in your source code, they are all represented by a single instance at runtime.

Q: What is a StringBuilder and what are its use cases? What is the difference between appending a string to a StringBuilder and concatenating two strings with a + operator? How does StringBuilder differ from StringBuffer?

StringBuilder allows manipulating character sequences by appending, deleting and inserting characters and strings. This is a mutable data structure, as opposed to the String class which is immutable.

When concatenating two String instances, a new object is created, and strings are copied. This could bring a huge garbage collector overhead if we need to create or modify a string in a loop. StringBuilder allows handling string manipulations much more efficiently.

StringBuffer is different from StringBuilder in that it is thread-safe. If you need to manipulate a string in a single thread, use StringBuilder instead.