My Java application has excessive garbage collection due to frequent object creation and disposal, so I want to implement an object pool to reuse objects and reduce GC overhead. How can I design a garbage-free object pool that dynamically adjusts its size (without creating garbage) based on application load? Can you provide an example implementation or guidance for pooling strategies, especially for handling pool growth?
- 4@SpeedChaser goal is to reduce load on GC, not increase it.talex– talex2024-12-23 11:06:30 +00:00Commented Dec 23, 2024 at 11:06
- 3Manual memory management in java is tricky. It mostly depends on what life-cycle your objects have. Are you sure you want to go this way, maybe fine tuning GC will be enough. You may try to profile your app and optimize hot spots. Switching to zero memory footprint most likely will require to redesign your whole app.talex– talex2024-12-23 11:10:19 +00:00Commented Dec 23, 2024 at 11:10
- 2Better to profile your app to determine which components are creating objects unnecessarily. eg objects allocated inside loops, and many adds to unsized collections such as HashMap / ArrayList.DuncG– DuncG2024-12-23 12:04:34 +00:00Commented Dec 23, 2024 at 12:04
- 5Just to be a devil's advocate: "I'm considering implementing an object pool that can reuse objects." -> the Java heap and garbage collector is an object pool that can reuse objects. If you write your own, make sure it's not less performant than the GC.k314159– k3141592024-12-23 12:53:19 +00:00Commented Dec 23, 2024 at 12:53
- 3@k314159 Sure, but writing a performant object pool is complex, and if not done correctly, probably worse than relying on GC. I'd say that in a lot of cases, like talex said, tuning GC is likely a better course of action before going down the route of writing an object pool.Mark Rotteveel– Mark Rotteveel2024-12-23 17:37:04 +00:00Commented Dec 23, 2024 at 17:37
2 Answers
The best data structure to use is a native array due to its better cache locality. The key challenge is to allow it to grow without generating garbage. This can be achieved by storing the previous array in a SoftReference:
int newSize = int (growthFactor * this.array.length); E[] oldArray = this.array; this.array = (E[]) new Object[newSize]; // copy from oldArray to this.array (if necessary) // nullify any object left on oldArray (if necessary) oldArrays.add(new SoftReference<E[]>(oldArray)); You can try implementing this approach yourself as an exercise, or you can use CoralPool, which is an open-source garbage-free solution optimized for performance. For a deeper dive into writing garbage-free Java code, check out this video.
On a side note, using java.util.LinkedList is not ideal because, like many other Java collections, it generates a significant amount of garbage. This creates a contradiction: you’re trying to minimize garbage using an object pool, but the pool itself contributes to garbage creation. Clearly, that's not an efficient approach. Run the code below to observe how much garbage it generates:
final LinkedList<Object> list = new LinkedList<>(); final Object object = new Object(); final Random random = new Random(); long start = System.currentTimeMillis(); for(int i = 0; i < 50_000; i++) { int toAdd = random.nextInt(1_000) + 1; int toRemove = random.nextInt(1_000) + 1; for(int x = 0; x < toAdd; x++) list.add(object); for(int x = 0; x < toRemove; x++) if (!list.isEmpty()) list.removeLast(); } long time = System.currentTimeMillis() - start; System.out.println("List size: " + list.size() + " after " + time + " millis");
$ java -verbose:gc -cp . TestLinkedList [0.003s][info][gc] Using G1 [0.034s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 25M->1M(388M) 0.796ms [0.043s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 29M->1M(388M) 1.587ms [0.060s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 61M->2M(388M) 2.989ms [0.082s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 78M->3M(388M) 4.149ms [0.107s][info][gc] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 111M->1M(820M) 1.293ms [0.149s][info][gc] GC(5) Pause Young (Normal) (G1 Evacuation Pause) 137M->2M(820M) 2.541ms List size: 38609 after 166 millis
Regarding this comment:
Having softly-refed objects doesn't delay a GC; it means that objects which are softly-refed might not be collected if the GC was able to free enough non-refed and weakly-refed objects. The typical use case is for caching the result of an expensive computation - by making the cache a soft-ref, you're telling the JVM that it is okay to throw out the cache (during a GC), if doing so allows you to avoid an OOM error. In your pool you are making the E-arrays garbage when you resize the array. This is a weird approach for a pool which wants "grow without generating garbage"
Below is a Java code that proves that GC is delayed by using a SoftReference, in other words, GC will only happen if your application comes under memory pressure, with the risk of crashing with an OutOfMemoryError. That's exactly the feature we want for our pool.
import java.lang.ref.SoftReference; import java.util.List; import java.util.ArrayList; public class SoftReferenceTest { static volatile boolean keepAllocating = true; public static class MyObject { private final int index; public final byte[] data = new byte[1024 * 1024]; public MyObject(int index) { this.index = index; } @Override public void finalize() { System.out.println("\nGarbage collected => " + index + " thread=" + Thread.currentThread()); // If EVEN then you are a SoftReference being collected if (index % 2 == 0) keepAllocating = false; // STOP LOOP DOWN BELOW } } public static void main(String[] args) throws InterruptedException { List<SoftReference<MyObject>> oldObjects = new ArrayList<>(); for(int i = 0; i < 4; i++) { MyObject myObject = new MyObject(i); if (i % 2 == 0) { // only add EVEN objects as SoftReferences oldObjects.add(new SoftReference<MyObject>(myObject)); } // ODD objects will go out-of-scope and be collected by the gc } // Force the GC and notice that the even objects are NOT collected System.out.println("Forcing the GC..."); System.gc(); Thread.sleep(100); // Now start allocating memory like crazy to force the // SoftReference to be collected List<byte[]> list = new ArrayList<>(1024); int counter = 0; int totalMemory = 0; int memoryToAllocate = 1024 * 128; System.out.println(); while(keepAllocating) { list.add(new byte[memoryToAllocate]); System.out.print("\r"); System.out.print(counter++); totalMemory += memoryToAllocate; System.out.print(" "); System.out.print(totalMemory); Thread.sleep(10); } } } Output:
$ java -Xms64m -Xmx64m -cp . SoftReferenceTest Forcing the GC... Garbage collected => 3 thread=Thread[#10,Finalizer,8,system] Garbage collected => 1 thread=Thread[#10,Finalizer,8,system] 384 50462720 <== App is running but SoftReference is not collected Garbage collected => 2 thread=Thread[#10,Finalizer,8,system] Garbage collected => 0 thread=Thread[#10,Finalizer,8,system] Note that we have successfully avoided the GC until the very end when the JVM was about to run out of memory.
384 50462720 => 50Mb when our max heap size was 64Mb Now run the test again, but this time with a larger max heap size:
$ java -Xms128m -Xmx128m -cp . SoftReferenceTest Forcing the GC... Garbage collected => 1 thread=Thread[#10,Finalizer,8,system] Garbage collected => 3 thread=Thread[#10,Finalizer,8,system] 847 111149056 <== App is running but SoftReference is not collected Garbage collected => 0 thread=Thread[#10,Finalizer,8,system] Garbage collected => 2 thread=Thread[#10,Finalizer,8,system] This time, because the JVM had more memory, it waited to collect the SoftReference until 111Mb of memory were allocated instead of only 50Mb.
847 111149056 => 111Mb when our max heap size was 128Mb That proves that a SoftReference is delayed until the JVM is about to run out of memory.
Note that if your application comes to a point that it is running out of memory and it is about to crash, of course it is much better to GC and try to avoid the OOM crash. That's exactly the purpose of the SoftReference and that's why we choose to use it for the pool.
Also note that you are not obligated to use a SoftReference. You can use a MultiArrayObjectPool or a TieredObjectPool which are pool implementations that do not discard arrays when they grow. Another important thing to note is that the pool, with a good enough initialCapacity and growthFactor, will grow very few times. Therefore it would also be perfectly fine to just hold the strong reference of the discarded array without wrapping it around a SoftReference, in this case really avoiding the GC forever, even when a OutOfMemoryError is about to crash your application.
I think the confusion lies in that fact that an object pool is not an object cache. They are two very different things. An object cache wants as much memory as possible, to cache as much as possible. An object pool just wants to reuse instances to avoid garbage. They have completely different purpose. In that sense, of an object pool and not an object cache, an OutOfMemoryError usually means a bug/leak in the application and should never happen.
Disclaimer: I'm one of the developers of the open-source project CoralPool.
19 Comments
SoftReference is that the GC will be delayed as much as possible, in other words, GC will only happen if the JVM starts to run out of memory, which should never happen in a well tuned application. When you say "The JVM will GC when it cannot satisfy a request to allocate memory for a new object" you are correct. The point is that this will never happen unless you are allocating objects like crazy and consuming all your available memory, which by itself is a problem. Memory is abundant and running out of it usually means a bug/leak in the app.SoftReference. I don't understand your concern when you say if doing so allows you to avoid an OOM error. My point is clear: in a normal and well-tuned application you should never reach a point of an OOM error, therefore the collection of the soft reference will be delayed forever.Perhaps something like this will serve as a starting point for you.
List<D> objectsEmpty = new ArrayList<>(); List<D> objectslist = new ArrayList<>(); String dat = "1,Tom:2,Jack:3,Willy:4,Sam"; void init() { String data[] = dat.split( ":" ); for( int i = 0; i < data.length; i ++ ) { objectslist.add( createObject( Integer.parseInt( data[ i ].split( "," )[ 0 ] ), data[ i ].split( "," )[ 1 ] ) ); } deleteObject( objectslist.remove( 2 ) ); objectslist.add( createObject( 12, "Hardy" ) ); for( int i = 0; i < objectslist.size(); i ++ ) { System.out.println( objectslist.get( i ).getName() ); } } void deleteObject( D obj ) { obj.setName( "" ); obj.setValue( 0 ); objectsEmpty.add( obj ); } D createObject( int value, String name ) { D newObject; if( objectsEmpty.isEmpty() ) { newObject = new D( value, name ); objectslist.add( newObject ); return newObject; } // modification suggested by k314159. newObject = objectsEmpty.remove( objectsEmpty.size() - 1 ); newObject.setName( name ); newObject.setValue( value ); return newObject; } class D { int value = 0; String name = ""; public D( int v, String n ) { value = v; name = n; } public int getValue() { return value; } public void setValue( int value ) { this.value = value; } public String getName() { return name; } public void setName( String name ) { this.name = name; } } 4 Comments
objectsEmpty.remove( 0 ) will shift all elements of the list to the left. It's much faster to do objectsEmpty.remove(objectsEmpty.size() - 1).