diff --git a/buffer/src/main/java/io/netty/buffer/PoolSubpage.java b/buffer/src/main/java/io/netty/buffer/PoolSubpage.java
--- a/buffer/src/main/java/io/netty/buffer/PoolSubpage.java
+++ b/buffer/src/main/java/io/netty/buffer/PoolSubpage.java
@@ -115,7 +115,13 @@ boolean free(PoolSubpage<T> head, int bitmapIdx) {
 
         if (numAvail ++ == 0) {
             addToPool(head);
-            return true;
+            /* When maxNumElems == 1, the maximum numAvail is also 1.
+             * Each of these PoolSubpages will go in here when they do free operation.
+             * If they return true directly from here, then the rest of the code will be unreachable
+             * and they will not actually be recycled. So return true only on maxNumElems > 1. */
+            if (maxNumElems > 1) {
+                return true;
+            }
         }
 
         if (numAvail != maxNumElems) {

diff --git a/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java b/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java
--- a/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java
+++ b/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java
@@ -632,8 +632,8 @@ void checkForError() throws Throwable {
     }
 
     @SuppressWarnings("unchecked")
-    private static PooledByteBuf<ByteBuffer> unwrapIfNeeded(ByteBuf buf) {
-        return (PooledByteBuf<ByteBuffer>) (buf instanceof PooledByteBuf ? buf : buf.unwrap());
+    private static <T> PooledByteBuf<T> unwrapIfNeeded(ByteBuf buf) {
+        return (PooledByteBuf<T>) (buf instanceof PooledByteBuf ? buf : buf.unwrap());
     }
 
     @Test
@@ -661,4 +661,27 @@ public void testCacheWorksForNormalAllocations() {
         assertEquals(3, allocator.metric().directArenas().get(0).numNormalAllocations());
         buffer.release();
     }
+
+    @Test
+    public void testNormalPoolSubpageRelease() {
+        // 16 < elemSize <= 7168 or 8192 < elemSize <= 28672, 1 < subpage.maxNumElems <= 256
+        // 7168 <= elemSize <= 8192, subpage.maxNumElems == 1
+        int elemSize = 8192;
+        int length = 1024;
+        ByteBuf[] byteBufs = new ByteBuf[length];
+        final PooledByteBufAllocator allocator = new PooledByteBufAllocator(false, 32, 32, 8192, 11, 256, 64, false, 0);
+
+        for (int i = 0; i < length; i++) {
+            byteBufs[i] = allocator.heapBuffer(elemSize, elemSize);
+        }
+        PoolChunk<Object> chunk = unwrapIfNeeded(byteBufs[0]).chunk;
+
+        int beforeFreeBytes = chunk.freeBytes();
+        for (int i = 0; i < length; i++) {
+            byteBufs[i].release();
+        }
+        int afterFreeBytes = chunk.freeBytes();
+
+        assertTrue(beforeFreeBytes < afterFreeBytes);
+    }
 }

When subpage.elemSize == 8192, the free subpage operation is not work
### Actual behavior
allocate N * 8192 subpage, free part of them, but subpage still esixt in two-way linked list 

### Minimal yet complete reproducer code (or URL to code)
```
io.netty.buffer.PoolSubpage#free {
    if (numAvail ++ == 0) { // numAvail always equals 0 when free subpage and elemSize == 8192
        addToPool(head);
        return true;
    }
}
```
### Netty version
4.1.52
### JVM version (e.g. `java -version`)
1.8
### OS version (e.g. `uname -a`)


@ZzxyNn are you interested in providing a PR (with a fix and maybe also a test case) ?
@normanmaurer
```

int oneLength = 16,twoLength = 256;
// int elemSize = 4096; // normal
int elemSize = 8192; // unnormal
ByteBuf[] ones = new ByteBuf[oneLength];
ByteBuf[] twos = new ByteBuf[twoLength];
//allocate
for (int i = 0; i < oneLength; i++) {
    ones[i] = ByteBufAllocator.DEFAULT.buffer(elemSize, Integer.MAX_VALUE);
}
for (int i = 0; i < twoLength; i++) {
    twos[i] = ByteBufAllocator.DEFAULT.buffer(elemSize, Integer.MAX_VALUE);
}

//deallocate the 'tows', in oder to filled of PoolThreadCache.MemoryRegionCache.queue
for (int i = 0; i < twoLength; i++) {
    new WrappedCompositeByteBuf(compositeBuffer().addComponent(twos[i])).deallocate();
}
// when deallocate ones, the MemoryRegionCache.queue had been filled, 
for (int i = 0; i < oneLength; i++) {
    new WrappedCompositeByteBuf(compositeBuffer().addComponent(ones[i])).deallocate();
}

```
elemSize = 8192:
when all of ByteBuf be freed, the size of PoolChunk#subpages will be 256 + 16, and PoolSubpage two-way linked list size = 16

elemSIze = 4196;
the size of PoolChunk#subpages will be 256 + 1, PoolSubpage two-way linked list size = 1
Please open a PR with a fix if you know how to solve this... We love contributions. 
@normanmaurer 
i have no idea yet....but you sure its a problem?
org.io.netty.buffer.PooledByteBufAllocatorTest