1. Strong Reference

Strong reference is the most common kind of reference. We use this kind of reference every day.

For example:

Foo firstReference = new Foo();
Foo secondReference = firstReference;

firstReference = null;
secondReference = null;
//Now the GC can collect an instance of the Foo, which created in the first line.

The created instance of the Foo class is not available for the garbage collector while there is at least one link to this object.

2. WeakReference

WeakReference - this is the type of references that will be removed by the garbage collector on the next pass, if there are no other type references to the object.

You can get an object value from the WeakReference until the GC decides to collect the object. As soon as the GC decides to do it (not after the GC finalize the object and clear an allocated memory), you will get the NULL from the WeakReference. This happens when the GC is just marking the object for a further processing. It is important to understand, that all finalization actions are executed only after this. When we look at the PhantomReference, we’ll return to this point.

Let’s try to testing WeakReference behavior:

@Test
public void testWeakAfterGC() {
    // Arrange
    String instance = new String("123");
    WeakReference<String> reference = new WeakReference<>(instance);

    // Act
    instance = null;
    System.gc();

    // Asserts
    Assertions.assertThat(reference.get()).isNull();
}

This test was successful.

Java provides us the WeakHashMap data structure. It’s a something like a HashMap, which uses the WeakReference as a key of the Map. If a key of the WeakHashMap becomes garbage, its entry is removed automatically.

How we can test a WeakHashMap behavior:

@Test
public void testWeakMap() throws InterruptedException {
    // Arrange
    WeakHashMap<String, Boolean> map = new WeakHashMap<>();
    String instance = new String("123");
    map.put(instance, true);

    // Act
    instance = null;
    GcUtils.fullFinalization();

    // Asserts
    Assertions.assertThat(map).isEmpty();
}

Results of weak tests:

result of weak tests

You might think that the WeakHashMap is a good solution for creating a cache, but you should understand what kind of behavior you need. Because if we are talking about a cache that removes data from a storage only when we reached a memory limit, then you should looking at the using of SoftReferences.

3. SoftReference

The behavior of SoftReference is similar to WeakReference, but GC collect this kind of reference only when our application does not have enough of memory.

Let’s try to test it:

@Test
public void softTest() {
    // Arrange
    String instance = new String("123323");
    SoftReference<String> softReference = new SoftReference<>(instance);
    instance = null;
    Assertions.assertThat(softReference).isNotNull();
    Assertions.assertThat(softReference.get()).isNotNull();

    // Act
    GcUtils.tryToAllocateAllAvailableMemory(); (1)

    // Asserts
    Assertions.assertThat(softReference.get()).isNull();
}
1 at this line we try to get all available memory until is the OutOfMemoryError thrown. I used GcUtils, this describes below.

The GC collects our SoftReference before we get the OutOfMemoryError.

This behavior is a good reason to use SoftReferences as a cache for a data that is difficult to build in memory.

For example, a reading video or graphics data from a slow file storage. When your application has enough of memory, you can receive this data from the cache, but if application reaches of a memory limit, then the cache cleans. And now, you need restore this data on the next request.

However in many cases you need to prefer a cache based on the LRU algorithm.

4. PhantomReference

The PhantomReferences are enqueued only when the object is physically removed from memory.

The get() method of the PhantomReference always returns NULL, specially to prevent you from being able to resurrect an almost removed object.

The PhantomReference provides you an ability to determine exactly when an object was removed from memory. In order for implementation this we need to work with a ReferenceQueue. When the referent object of a PhantomReference, removes from a memory then the GC enqueues the phantomReference in the referenceQueue and we can poll this reference from this queue.

Let’s look at the code:

@Test
public void testQueuePollAfterFinalizationGC() {
  // Arrange
  Foo foo = new Foo();
  ReferenceQueue<Foo> referenceQueue = new ReferenceQueue<>();
  PhantomReference<Foo> phantomReference = new PhantomReference<>(foo, referenceQueue);

  // Act
  foo = null;
  GcUtils.fullFinalization();

  // Asserts
  Assertions.assertThat(phantomReference.isEnqueued()).isTrue();
  Assertions.assertThat(referenceQueue.poll()).isEqualTo(phantomReference);
}

private class Foo {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("FIN!");
        super.finalize();
    }
}
I used my utility class (GcUtils.java), to test the behavior of phantom references. This tools are ensure that the GC accurately performs a full cycle of cleaning and finalization. You can find source code of this utility on my github: github.com/antkorwin/common-utils

For example, if you need to allocate memory for processing of a large video file, only after finish processing of a previous file and release memory (that was allocated in the previous step) then using a PhantomReference is a right decision. Because you can request a new part of memory exactly after the GC is release a previous used part. So, this reduces a chance to get the OutOfMemoryError when you already released a part of memory but the GC has not collected it yet.

Finalization may does not happened in a timely fashion, it is difficult to predict the number of garbage collection cycles while the object was waiting for finalization. It can lead to serious delay in actually cleaning up garbage objects, and become the reason of you can get an OutOfMemoryErrors even when most of the heap is garbage.

I wrote a simple utility to checking a code on memory leaks, which is using phantom references:

Example of using this tool:

@Test
public void testWithoutLeaks() {
    // Arrange
    Foo foo = new Foo();
    LeakDetector leakDetector = new LeakDetector(foo);

    // Act
    foo = null;

    // Asserts
    leakDetector.assertMemoryLeaksNotExist();
}

@Test
public void testWithLeak() {
    // Arrange
    Foo foo = new Foo();
    Foo bar = foo;
    LeakDetector leakDetector = new LeakDetector(foo);

    // Act
    foo = null;

    // Asserts
    leakDetector.assertMemoryLeaksExist();
}

Let’s look at the code of the memory leaks detector:

public class LeakDetector extends PhantomReference<Object> {

    private final String description;

    /**
     * Initialization of the memory leaks detector.
     * @param referent the object(resource) for which we are looking for leaks.
     */
    public LeakDetector(Object referent) {
        super(referent, new ReferenceQueue<>());
        this.description = String.valueOf(referent);
    }

    /**
     * If exists memory leaks then throws a fail.
     *
     * WARN: Run this method after delete all references on the checkable object(resource)
     */
    public void assertMemoryLeaksNotExist() {
        GcUtils.fullFinalization();

        Assertions.assertThat(isEnqueued())
                  .as("Object: " + description + " was leaked")
                  .isTrue();
    }

    /**
     * If not exists memory leaks then throws a fail.
     *
     * WARN: Run this method after delete all references on the checkable object(resource)
     */
    public void assertMemoryLeaksExist() {
        GcUtils.fullFinalization();

        Assertions.assertThat(isEnqueued())
                  .as("Object: " + description + " already collected by the GC")
                  .isFalse();
    }
}

5. Source code of this project on the github