diff --git a/community/io/src/main/java/org/neo4j/io/mem/GrabAllocator.java b/community/io/src/main/java/org/neo4j/io/mem/GrabAllocator.java index 7b9d41dc7efa3..6d65087823cca 100644 --- a/community/io/src/main/java/org/neo4j/io/mem/GrabAllocator.java +++ b/community/io/src/main/java/org/neo4j/io/mem/GrabAllocator.java @@ -19,6 +19,8 @@ */ package org.neo4j.io.mem; +import sun.misc.Cleaner; + import org.neo4j.memory.MemoryAllocationTracker; import org.neo4j.unsafe.impl.internal.dragons.UnsafeUtil; @@ -31,18 +33,9 @@ */ public final class GrabAllocator implements MemoryAllocator { - /** - * The amount of memory, in bytes, to grab in each Grab. - */ - private static final long GRAB_SIZE = getInteger( GrabAllocator.class, "GRAB_SIZE", (int) kibiBytes( 512 ) ); - - /** - * The amount of memory that this memory manager can still allocate. - */ - private long memoryReserve; - private final MemoryAllocationTracker memoryTracker; - - private Grab grabs; + private final Grabs grabs; + @SuppressWarnings( {"unused", "FieldCanBeLocal"} ) + private final Cleaner cleaner; /** * Create a new GrabAllocator that will allocate the given amount of memory, to pointers that are aligned to the @@ -53,126 +46,32 @@ public final class GrabAllocator implements MemoryAllocator */ GrabAllocator( long expectedMaxMemory, MemoryAllocationTracker memoryTracker ) { - this.memoryReserve = expectedMaxMemory; - this.memoryTracker = memoryTracker; + this.grabs = new Grabs( expectedMaxMemory, memoryTracker ); + this.cleaner = Cleaner.create( this, new GrabsDeallocator( grabs ) ); } @Override public synchronized long usedMemory() { - long sum = 0; - Grab grab = grabs; - while ( grab != null ) - { - sum += grab.nextPointer - grab.address; - grab = grab.next; - } - return sum; + return grabs.usedMemory(); } @Override public synchronized long availableMemory() { - Grab grab = grabs; - long availableInCurrentGrab = 0; - if ( grab != null ) - { - availableInCurrentGrab = grab.limit - grab.nextPointer; - } - return Math.max( memoryReserve, 0L ) + availableInCurrentGrab; + return grabs.availableMemory(); } @Override public synchronized long allocateAligned( long bytes, long alignment ) { - if ( alignment <= 0 ) - { - throw new IllegalArgumentException( "Invalid alignment: " + alignment + ". Alignment must be positive." ); - } - long grabSize = Math.min( GRAB_SIZE, memoryReserve ); - try - { - if ( bytes > GRAB_SIZE ) - { - // This is a huge allocation. Put it in its own grab and keep any existing grab at the head. - grabSize = bytes; - Grab nextGrab = grabs == null ? null : grabs.next; - Grab allocationGrab = new Grab( nextGrab, grabSize, memoryTracker ); - if ( !allocationGrab.canAllocate( bytes, alignment ) ) - { - allocationGrab.free(); - grabSize = bytes + alignment; - allocationGrab = new Grab( nextGrab, grabSize, memoryTracker ); - } - long allocation = allocationGrab.allocate( bytes, alignment ); - grabs = grabs == null ? allocationGrab : grabs.setNext( allocationGrab ); - memoryReserve -= bytes; - return allocation; - } - - if ( grabs == null || !grabs.canAllocate( bytes, alignment ) ) - { - if ( grabSize < bytes ) - { - grabSize = bytes; - Grab grab = new Grab( grabs, grabSize, memoryTracker ); - if ( grab.canAllocate( bytes, alignment ) ) - { - memoryReserve -= grabSize; - grabs = grab; - return grabs.allocate( bytes, alignment ); - } - grab.free(); - grabSize = bytes + alignment; - } - grabs = new Grab( grabs, grabSize, memoryTracker ); - memoryReserve -= grabSize; - } - return grabs.allocate( bytes, alignment ); - } - catch ( OutOfMemoryError oome ) - { - NativeMemoryAllocationRefusedError error = - new NativeMemoryAllocationRefusedError( grabSize, usedMemory() ); - initCause( error, oome ); - throw error; - } - } - - private void initCause( NativeMemoryAllocationRefusedError error, OutOfMemoryError cause ) - { - try - { - error.initCause( cause ); - } - catch ( Throwable ignore ) - { - // This can only happen if our NMARE somehow already has a cause initialised, which should not - // be the case, but it could if the JDK decided to inject a default cause in some future version. - // To avoid loosing the ability to trace this cause back, we'll add it as a suppressed exception - // instead. - try - { - error.addSuppressed( cause ); - } - catch ( Throwable ignore2 ) - { - // Okay, we tried. - } - } + return grabs.allocateAligned( bytes, alignment ); } @Override - protected synchronized void finalize() throws Throwable + public void close() { - super.finalize(); - Grab current = grabs; - - while ( current != null ) - { - current.free(); - current = current.next; - } + cleaner.clean(); } private static class Grab @@ -246,4 +145,152 @@ public String toString() return String.format( "Grab[size = %d bytes, reserve = %d bytes, use = %5.2f %%]", size, reserve, use ); } } + + private static final class Grabs + { + /** + * The amount of memory, in bytes, to grab in each Grab. + */ + private static final long GRAB_SIZE = getInteger( GrabAllocator.class, "GRAB_SIZE", (int) kibiBytes( 512 ) ); + + private final MemoryAllocationTracker memoryTracker; + private long expectedMaxMemory; + private Grab head; + + Grabs( long expectedMaxMemory, MemoryAllocationTracker memoryTracker ) + { + this.expectedMaxMemory = expectedMaxMemory; + this.memoryTracker = memoryTracker; + } + + long usedMemory() + { + long sum = 0; + Grab grab = head; + while ( grab != null ) + { + sum += grab.nextPointer - grab.address; + grab = grab.next; + } + return sum; + } + + long availableMemory() + { + Grab grab = head; + long availableInCurrentGrab = 0; + if ( grab != null ) + { + availableInCurrentGrab = grab.limit - grab.nextPointer; + } + return Math.max( expectedMaxMemory, 0L ) + availableInCurrentGrab; + } + + public void close() + { + Grab current = head; + + while ( current != null ) + { + current.free(); + current = current.next; + } + head = null; + } + + long allocateAligned( long bytes, long alignment ) + { + if ( alignment <= 0 ) + { + throw new IllegalArgumentException( "Invalid alignment: " + alignment + ". Alignment must be positive." ); + } + long grabSize = Math.min( GRAB_SIZE, expectedMaxMemory ); + try + { + if ( bytes > GRAB_SIZE ) + { + // This is a huge allocation. Put it in its own grab and keep any existing grab at the head. + grabSize = bytes; + Grab nextGrab = head == null ? null : head.next; + Grab allocationGrab = new Grab( nextGrab, grabSize, memoryTracker ); + if ( !allocationGrab.canAllocate( bytes, alignment ) ) + { + allocationGrab.free(); + grabSize = bytes + alignment; + allocationGrab = new Grab( nextGrab, grabSize, memoryTracker ); + } + long allocation = allocationGrab.allocate( bytes, alignment ); + head = head == null ? allocationGrab : head.setNext( allocationGrab ); + expectedMaxMemory -= bytes; + return allocation; + } + + if ( head == null || !head.canAllocate( bytes, alignment ) ) + { + if ( grabSize < bytes ) + { + grabSize = bytes; + Grab grab = new Grab( head, grabSize, memoryTracker ); + if ( grab.canAllocate( bytes, alignment ) ) + { + expectedMaxMemory -= grabSize; + head = grab; + return head.allocate( bytes, alignment ); + } + grab.free(); + grabSize = bytes + alignment; + } + head = new Grab( head, grabSize, memoryTracker ); + expectedMaxMemory -= grabSize; + } + return head.allocate( bytes, alignment ); + } + catch ( OutOfMemoryError oome ) + { + NativeMemoryAllocationRefusedError error = + new NativeMemoryAllocationRefusedError( grabSize, usedMemory() ); + initCause( error, oome ); + throw error; + } + } + + private static void initCause( NativeMemoryAllocationRefusedError error, OutOfMemoryError cause ) + { + try + { + error.initCause( cause ); + } + catch ( Throwable ignore ) + { + // This can only happen if our NMARE somehow already has a cause initialised, which should not + // be the case, but it could if the JDK decided to inject a default cause in some future version. + // To avoid loosing the ability to trace this cause back, we'll add it as a suppressed exception + // instead. + try + { + error.addSuppressed( cause ); + } + catch ( Throwable ignore2 ) + { + // Okay, we tried. + } + } + } + } + + private static final class GrabsDeallocator implements Runnable + { + private final Grabs grabs; + + GrabsDeallocator( Grabs grabs ) + { + this.grabs = grabs; + } + + @Override + public void run() + { + grabs.close(); + } + } } diff --git a/community/io/src/main/java/org/neo4j/io/mem/MemoryAllocator.java b/community/io/src/main/java/org/neo4j/io/mem/MemoryAllocator.java index 856d05667817c..96448ef522cec 100644 --- a/community/io/src/main/java/org/neo4j/io/mem/MemoryAllocator.java +++ b/community/io/src/main/java/org/neo4j/io/mem/MemoryAllocator.java @@ -50,4 +50,9 @@ static MemoryAllocator createAllocator( String expectedMemory, MemoryAllocationT * @throws OutOfMemoryError if the requested memory could not be allocated. */ long allocateAligned( long bytes, long alignment ); + + /** + * Close all allocated resources and free all allocated memory. + */ + void close(); } diff --git a/community/io/src/main/java/org/neo4j/io/pagecache/impl/muninn/PageList.java b/community/io/src/main/java/org/neo4j/io/pagecache/impl/muninn/PageList.java index 61494732c3987..3ecef5429a392 100644 --- a/community/io/src/main/java/org/neo4j/io/pagecache/impl/muninn/PageList.java +++ b/community/io/src/main/java/org/neo4j/io/pagecache/impl/muninn/PageList.java @@ -535,4 +535,9 @@ void toString( long pageRef, StringBuilder sb ) sb.append( ", usageCounter = " ).append( getUsageCounter( pageRef ) ); sb.append( " ] " ).append( OffHeapPageLock.toString( offLock( pageRef ) ) ); } + + public void close() + { + memoryAllocator.close(); + } } diff --git a/community/io/src/test/java/org/neo4j/io/mem/MemoryAllocatorTest.java b/community/io/src/test/java/org/neo4j/io/mem/MemoryAllocatorTest.java index dad86de79de62..122669b147ad0 100644 --- a/community/io/src/test/java/org/neo4j/io/mem/MemoryAllocatorTest.java +++ b/community/io/src/test/java/org/neo4j/io/mem/MemoryAllocatorTest.java @@ -170,7 +170,7 @@ public void trackMemoryAllocations() throws Throwable assertEquals( ByteUnit.mebiBytes( 1 ), memoryTracker.usedDirectMemory() ); //noinspection FinalizeCalledExplicitly - allocator.finalize(); + allocator.close(); assertEquals( 0, memoryTracker.usedDirectMemory() ); } } diff --git a/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTest.java b/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTest.java index 6ce2a3b53d0c1..813efe745fca2 100644 --- a/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTest.java +++ b/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTest.java @@ -2798,7 +2798,7 @@ public void writeLockedPageMustBlockFileUnmapping() throws Exception PageCursor cursor = pagedFile.io( 0, PF_SHARED_WRITE_LOCK ); assertTrue( cursor.next() ); - Thread unmapper = fork( $close( pagedFile ) ); + Thread unmapper = fork( closePageFile( pagedFile ) ); unmapper.join( 100 ); cursor.close(); @@ -2816,7 +2816,7 @@ public void optimisticReadLockedPageMustNotBlockFileUnmapping() throws Exception PageCursor cursor = pagedFile.io( 0, PF_SHARED_READ_LOCK ); assertTrue( cursor.next() ); // Got a read lock - fork( $close( pagedFile ) ).join(); + fork( closePageFile( pagedFile ) ).join(); cursor.close(); } @@ -2832,7 +2832,7 @@ public void advancingPessimisticReadLockingCursorAfterUnmappingMustThrow() throw PageCursor cursor = pagedFile.io( 0, PF_SHARED_READ_LOCK ); assertTrue( cursor.next() ); // Got a pessimistic read lock - fork( $close( pagedFile ) ).join(); + fork( closePageFile( pagedFile ) ).join(); expectedException.expect( FileIsNotMappedException.class ); cursor.next(); @@ -2851,7 +2851,7 @@ public void advancingOptimisticReadLockingCursorAfterUnmappingMustThrow() throws assertTrue( cursor.next() ); // fault + unpin page 0 assertTrue( cursor.next( 0 ) ); // potentially optimistic read lock page 0 - fork( $close( pagedFile ) ).join(); + fork( closePageFile( pagedFile ) ).join(); try { @@ -2877,7 +2877,7 @@ public void readingAndRetryingOnPageWithOptimisticReadLockingAfterUnmappingMustN assertTrue( cursor.next() ); // fault + unpin page 0 assertTrue( cursor.next( 0 ) ); // potentially optimistic read lock page 0 - fork( $close( pagedFile ) ).join(); + fork( closePageFile( pagedFile ) ).join(); pageCache.close(); pageCache = null; diff --git a/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTestSupport.java b/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTestSupport.java index ac448b2a8c78a..04e050272720e 100644 --- a/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTestSupport.java +++ b/community/io/src/test/java/org/neo4j/io/pagecache/PageCacheTestSupport.java @@ -350,7 +350,7 @@ protected void verifyRecordsInFile( ReadableByteChannel channel, int recordCount } } - protected Runnable $close( final PagedFile file ) + protected Runnable closePageFile( final PagedFile file ) { return () -> {