In this article we will compare Java Hard Reference, Weak Reference, Soft Reference, Phantom Reference along with example and monitor heap usage & garbage collection behavior for these references under heavy load.
How will we test heap usage & GC behavior?
- Create Lots of object: We will create lots of custom objects with heavy data in it so that it will consume observable heap memory.
- Create References: We will point hard/soft/weak/phantom references to above objects.
- Retain all references: We will put all above references in a static ArrayList instance so that all references will be retained in heap till program is running.
- Test with limited memory: We will test this program with limited heap memory of 50 MB so that we will have smaller observation set. We will use these java options for this -Xmx50m -XX:MaxMetaspaceSize=50m
- Monitor heap & GC: As per the documentation of references, objects might be garbage collected as per specification. We will use JMX console to monitor heap usage & garbage collection behavior.
We will use object of below class to point reference to. These objects will have a ‘heavyLoad’ which is just a huge String made from ‘name’. This is to make object heavy so that we can observe heap usage & GC behavior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class MyObject { private String name; private String heavyLoad = ""; public MyObject(String name) { this.name = name; for (int i = 0; i < 2000; i++) { heavyLoad = heavyLoad + name; } System.out.println("Created - " + name); } @Override protected void finalize() throws Throwable { // Print during finalization to understand when object is collected. System.out.println("#### Finalizing - " + name); } } |
Hard Reference
Hard reference is most basic variable reference in Java. As long as these references are retained in any way, objects are not considered eligible for garbage collection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import java.util.ArrayList; import java.util.List; public class HardReferenceExamples { public static List<MyObject> hardReferences = new ArrayList<>(); public static void main(String[] args) throws InterruptedException { // Give some time to open JCONSOLE. System.out.println("Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs."); Thread.sleep(30000); System.out.println("Starting now ..."); for (int i = 0; i < 100000; i++) { // Create hard reference with heavy object & add to list to retain MyObject hardObj = new MyObject("Hard-MyObject-" + i); hardReferences.add(hardObj); } System.out.println("Completed !"); } } |
Execute program with 50 MB heap & same metaspace.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
java -Xmx50m -XX:MaxMetaspaceSize=50m HardReferenceExamples Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs. Starting now ... Created - Hard-MyObject-0 Created - Hard-MyObject-1 Created - Hard-MyObject-2 . <-- logs skipped for this article --> . Created - Hard-MyObject-1089 #### Finalizing - Hard-MyObject-1090 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray(Unsafe.java:1269) at java.base/java.lang.invoke.StringConcatFactory$MethodHandleInlineCopyStrategy.newArray(StringConcatFactory.java:1633) at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder) at java.base/java.lang.invoke.LambdaForm$MH/0x00000001000cdc40.invoke(LambdaForm$MH) at java.base/java.lang.invoke.Invokers$Holder.linkToTargetMethod(Invokers$Holder) at MyObject.<init>(HardReferenceExamples.java:34) at HardReferenceExamples.main(HardReferenceExamples.java:17) |
Program waits 30 seconds at start so that you can get enough time to open JMX console. Open jconsole.exe from JDK (On windows, C:\Program Files\Java\jdk-11.0.1\bin). Connect to running program, go to ‘Memory’ tab & select “Heap Memory Usage” in chart dropdown.
Observations:
- You can see in above graph, as program continues to run, heap usage kept on growing & there is almost no drop in heap usage anywhere.
- Eventually program ended with java.lang.OutOfMemoryError: Java heap space & JVM ended.
- You can see that JVM spent entire 1 minute on OLD generation GC collection out of ~3 min program which shows that GC had hard time collecting objects.
- There were 2733 GC old collection but as per graph there was no luck in collecting any objects (as expected in this example)
Soft Reference
Soft references are special references in Java. Objects referenced by SoftReference are supposed to be biased against clearing recently created soft reference. As per javadoc of SoftReference, “Virtual machine implementations are, however, encouraged to bias against clearing recently-created or recently-used soft references.”. So its safe to say that as long as object of only softly referenced & not recently used or created, it will be eligible for garbage collection & finalization.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.List; public class SoftReferenceExamples { public static List<SoftReference<MyObject>> softReferences = new ArrayList<>(); public static void main(String[] args) throws InterruptedException { // Give some time to open JCONSOLE. System.out.println("Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs."); Thread.sleep(30000); System.out.println("Starting now ..."); for (int i = 0; i < 100000; i++) { // Create soft reference with heavy object & add to list to retain MyObject softObj = new MyObject("Soft-MyObject-" + i); softReferences.add(new SoftReference<MyObject>(softObj)); } System.out.println("Completed !"); } } |
Execute program with 50 MB heap & metaspace.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
java -Xmx50m -XX:MaxMetaspaceSize=50m SoftReferenceExamples Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs. Starting now ... Created - Soft-MyObject-0 Created - Soft-MyObject-1 Created - Soft-MyObject-2 Created - Soft-MyObject-3 Created - Soft-MyObject-4 Created - Soft-MyObject-5 Created - Soft-MyObject-6 . <-- logs skipped for this article --> . |
Observations:
- You can see in above graph, as program continues to run, heap usage kept on peaking beyond 40 MB & then dropped several times due to GC collections. So JVM did not collect objects unless heap usage was nearing 40MB & above i.e. closer to max heap 50 MB.
- Program continued execution without any issues but objects started finalizing bit later in time as per logs.
- You can see that JVM spent more time in young generation (May be collecting lot of Stings we created) i.e. 1 minute with whopping ~78 thousands collections in span of ~ 4 minutes.
- There were several objects made it to old generation (most probably soft references) & they were easily collected in just 36 collections which took ~2 seconds. So GC did not have much difficulty.
Weak Reference
Objects pointed by weak references are made eligible for garbage collection when only referenced by weak reference without any bias like soft reference. So it is upto JVM to unconditionally decide when to collect weak referenced object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; public class WeakReferenceExamples { public static List<WeakReference<MyObject>> weakReferences = new ArrayList<>(); public static void main(String[] args) throws InterruptedException { // Give some time to open JCONSOLE. System.out.println("Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs."); Thread.sleep(30000); System.out.println("Starting now ..."); for (int i = 0; i < 100000; i++) { // Create weak reference with heavy object & add to list to retain MyObject weakObj = new MyObject("Weak-MyObject-" + i); weakReferences.add(new WeakReference<MyObject>(weakObj)); } System.out.println("Completed !"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
java -Xmx50m -XX:MaxMetaspaceSize=50m WeakReferenceExamples Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs. Starting now ... Created - Weak-MyObject-0 Created - Weak-MyObject-1 #### Finalizing - Weak-MyObject-1 Created - Weak-MyObject-2 #### Finalizing - Weak-MyObject-2 Created - Weak-MyObject-3 #### Finalizing - Weak-MyObject-3 . <-- logs skipped for this article --> . |
Observations:
- You can see in above graph, as program continues to run, heap usage kept on peaking anywhere between 20 MB till ~40 MB. Heap usage dropped even when it was below 30 MB. There are several drops below 40 MB or even below 30 MB which means GC happened even when it wasn’t nearing max heap usage.
- Program continued execution without any issues & objects started finalizing almost immediately as per logs.
- You can see that JVM spent ~30 seconds in young generation collection with ~ 32 thousands collections.
- There were literally 0 collections in old generation as probably most of the weak references were getting collected in young generation.
Phantom Reference
As per javadoc documentation of PhantomReference, “Phantom references are most often used to schedule post-mortem cleanup actions.”. Phantom reference objects are mainly meant for cleanup so they are eligible to garbage collection & finalization almost immidiately after being only referenced by phantom reference. You can never retrieve object from phantom reference.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.lang.ref.PhantomReference; import java.util.ArrayList; import java.util.List; public class PhantomReferenceExamples { public static List<PhantomReference<MyObject>> phantomReferences = new ArrayList<>(); public static void main(String[] args) throws InterruptedException { // Give some time to open JCONSOLE. System.out.println("Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs."); Thread.sleep(30000); System.out.println("Starting now ..."); for (int i = 0; i < 100000; i++) { // Create phantom reference with heavy object & add to list to retain MyObject phantomObj = new MyObject("Phantom-MyObject-" + i); phantomReferences.add(new PhantomReference<MyObject>(phantomObj, null)); } System.out.println("Completed !"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
java -Xmx50m -XX:MaxMetaspaceSize=50m PhantomReferenceExamples Please open JCONSOLE Heap Memory Graph. Waiting for 30 secs. Starting now ... Created - Phantom-MyObject-0 Created - Phantom-MyObject-1 #### Finalizing - Phantom-MyObject-1 Created - Phantom-MyObject-2 #### Finalizing - Phantom-MyObject-2 Created - Phantom-MyObject-3 #### Finalizing - Phantom-MyObject-3 Created - Phantom-MyObject-4 #### Finalizing - Phantom-MyObject-4 Created - Phantom-MyObject-5 . <-- logs skipped for this article --> . |
Observations:
- You can see in above graph, as program continues to run, heap usage kept on peaking anywhere between 20 MB till ~40 MB. Heap usage drops seems more frequent than weak references.
- Program continued execution without any issues & objects started finalizing almost immediately as per logs.
- You can see that JVM spent ~35 seconds in young generation collection with ~ 38 thousands collections.
- There are literally 0 collections in old generation as probably most of the phantom references were getting collected in young generation.
Watch test in action
Here is a video of execution of all above tests with running logs & live heap usage graph in JMX console. Graphs might vary a bit as its a separate run.