[IGNITE-21999] Merge partition free-lists into one (#3615)

diff --git a/check-rules/spotbugs-excludes.xml b/check-rules/spotbugs-excludes.xml
index a548ea0..27dec94 100644
--- a/check-rules/spotbugs-excludes.xml
+++ b/check-rules/spotbugs-excludes.xml
@@ -251,7 +251,7 @@
   <Match>
     <!-- TODO: https://issues.apache.org/jira/browse/IGNITE-21692 -->
     <Bug pattern="UPM_UNCALLED_PRIVATE_METHOD"/>
-    <Class name="org.apache.ignite.internal.pagememory.freelist.AbstractFreeList"/>
+    <Class name="org.apache.ignite.internal.pagememory.freelist.FreeListImpl"/>
     <Method name="initReusedPage"/>
   </Match>
   <Match>
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/PageMemoryIoModule.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/PageMemoryIoModule.java
index ac99bbf..18cdef9 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/PageMemoryIoModule.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/PageMemoryIoModule.java
@@ -22,6 +22,7 @@
 import java.util.List;
 import org.apache.ignite.internal.pagememory.freelist.io.PagesListMetaIo;
 import org.apache.ignite.internal.pagememory.freelist.io.PagesListNodeIo;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
 import org.apache.ignite.internal.pagememory.io.IoVersions;
 import org.apache.ignite.internal.pagememory.io.PageIoModule;
 import org.apache.ignite.internal.pagememory.persistence.io.PartitionMetaIo;
@@ -37,7 +38,8 @@
         return List.of(
                 PagesListMetaIo.VERSIONS,
                 PagesListNodeIo.VERSIONS,
-                PartitionMetaIo.VERSIONS
+                PartitionMetaIo.VERSIONS,
+                DataPageIo.VERSIONS
         );
     }
 }
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/Storable.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/Storable.java
index c692813..52b3468 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/Storable.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/Storable.java
@@ -17,14 +17,19 @@
 
 package org.apache.ignite.internal.pagememory;
 
+import java.nio.ByteBuffer;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
-import org.apache.ignite.internal.pagememory.io.IoVersions;
 
 /**
  * Simple interface for data, store in some RowStore.
  */
 public interface Storable {
+    /** Number of bytes a data type takes in storage. */
+    int DATA_TYPE_SIZE_BYTES = 1;
+
+    /** Offset of data type from the beginning of the row. */
+    int DATA_TYPE_OFFSET = 0;
+
     /**
      * Sets link for this row.
      *
@@ -57,7 +62,51 @@
     int headerSize();
 
     /**
-     * Returns I/O for handling this storable.
+     * Writes the row.
+     *
+     * @param pageAddr Page address.
+     * @param dataOff Data offset.
+     * @param payloadSize Payload size.
+     * @param newRow {@code False} if existing cache entry is updated, in this case skip key data write.
      */
-    IoVersions<? extends AbstractDataPageIo<?>> ioVersions();
+    void writeRowData(
+            long pageAddr,
+            int dataOff,
+            int payloadSize,
+            boolean newRow
+    );
+
+    /**
+     * Writes row data fragment.
+     *
+     * @param pageBuf Byte buffer.
+     * @param rowOff Offset in row data bytes.
+     * @param payloadSize Data length that should be written in a fragment.
+     */
+    void writeFragmentData(
+            ByteBuffer pageBuf,
+            int rowOff,
+            int payloadSize
+    );
+
+    /**
+     * Writes content of the byte buffer into the page.
+     *
+     * @param pageBuffer Direct page buffer.
+     * @param valueBuffer Byte buffer with value bytes.
+     * @param offset Offset within the value buffer.
+     * @param payloadSize Number of bytes to write.
+     */
+    static void putValueBufferIntoPage(ByteBuffer pageBuffer, ByteBuffer valueBuffer, int offset, int payloadSize) {
+        int oldPosition = valueBuffer.position();
+        int oldLimit = valueBuffer.limit();
+
+        valueBuffer.position(offset);
+        valueBuffer.limit(offset + payloadSize);
+
+        pageBuffer.put(valueBuffer);
+
+        valueBuffer.position(oldPosition);
+        valueBuffer.limit(oldLimit);
+    }
 }
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/DataPageReader.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/DataPageReader.java
index d72223d..c6a0afd 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/DataPageReader.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/DataPageReader.java
@@ -22,7 +22,7 @@
 
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.pagememory.PageMemory;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
 import org.apache.ignite.internal.pagememory.io.DataPagePayload;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
 import org.jetbrains.annotations.Nullable;
@@ -77,7 +77,7 @@
                 assert pageAddr != 0L : currentLink;
 
                 try {
-                    AbstractDataPageIo<?> dataIo = pageMemory.ioRegistry().resolve(pageAddr);
+                    DataPageIo dataIo = pageMemory.ioRegistry().resolve(pageAddr);
 
                     int itemId = itemId(currentLink);
 
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/NonFragmentableDataPageReader.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/NonFragmentableDataPageReader.java
index 476efad..1e6ba1c 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/NonFragmentableDataPageReader.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/NonFragmentableDataPageReader.java
@@ -22,7 +22,7 @@
 
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.pagememory.PageMemory;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
 import org.apache.ignite.internal.pagememory.io.DataPagePayload;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
 import org.jetbrains.annotations.Nullable;
@@ -73,7 +73,7 @@
             assert pageAddr != 0L : link;
 
             try {
-                AbstractDataPageIo<?> dataIo = pageMemory.ioRegistry().resolve(pageAddr);
+                DataPageIo dataIo = pageMemory.ioRegistry().resolve(pageAddr);
 
                 int itemId = itemId(link);
 
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/ReadPageMemoryRowValue.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/ReadPageMemoryRowValue.java
index 20d801e..6c8c85a 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/ReadPageMemoryRowValue.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datapage/ReadPageMemoryRowValue.java
@@ -61,6 +61,8 @@
     }
 
     private long readFullyOrStartReadingFragmented(long pageAddr, DataPagePayload payload) {
+        assert PageUtils.getByte(pageAddr, payload.offset() + Storable.DATA_TYPE_OFFSET) == dataType();
+
         valueSize = readValueSize(pageAddr, payload);
 
         if (!payload.hasMoreFragments()) {
@@ -75,6 +77,9 @@
         }
     }
 
+    /** Returns type of the data row. */
+    protected abstract byte dataType();
+
     private int readValueSize(long pageAddr, DataPagePayload payload) {
         return PageUtils.getInt(pageAddr, payload.offset() + valueSizeOffsetInFirstSlot());
     }
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datastructure/DataStructure.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datastructure/DataStructure.java
index d5ca544..a3c8970 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datastructure/DataStructure.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/datastructure/DataStructure.java
@@ -436,7 +436,7 @@
      * @return Page ID with the incremented rotation ID.
      * @see FullPageId
      */
-    protected final long recyclePage(long pageId, long pageAddr) {
+    protected static long recyclePage(long pageId, long pageAddr) {
         long recycled = 0;
 
         if (flag(pageId) == FLAG_DATA) {
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java
index a0a0bfb..e2d4dfb 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeList.java
@@ -21,30 +21,27 @@
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.pagememory.Storable;
-import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
 import org.apache.ignite.internal.pagememory.util.PageHandler;
 
 /**
  * Free list.
  */
-public interface FreeList<T extends Storable> {
+public interface FreeList {
     /**
      * Inserts a row.
      *
      * @param row Row.
-     * @param statHolder Statistics holder to track IO operations.
      * @throws IgniteInternalCheckedException If failed.
      */
-    void insertDataRow(T row, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException;
+    void insertDataRow(Storable row) throws IgniteInternalCheckedException;
 
     /**
      * Inserts rows.
      *
      * @param rows Rows.
-     * @param statHolder Statistics holder to track IO operations.
      * @throws IgniteInternalCheckedException If failed.
      */
-    void insertDataRows(Collection<T> rows, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException;
+    void insertDataRows(Collection<? extends Storable> rows) throws IgniteInternalCheckedException;
 
     /**
      * Updates a row by link.
@@ -54,25 +51,22 @@
      * @param arg Handler argument.
      * @param <S> Argument type.
      * @param <R> Result type.
-     * @param statHolder Statistics holder to track IO operations.
      * @return Result.
      * @throws IgniteInternalCheckedException If failed.
      */
     <S, R> R updateDataRow(
             long link,
             PageHandler<S, R> pageHnd,
-            S arg,
-            IoStatisticsHolder statHolder
+            S arg
     ) throws IgniteInternalCheckedException;
 
     /**
      * Removes a row by link.
      *
      * @param link Row link.
-     * @param statHolder Statistics holder to track IO operations.
      * @throws IgniteInternalCheckedException If failed.
      */
-    void removeDataRowByLink(long link, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException;
+    void removeDataRowByLink(long link) throws IgniteInternalCheckedException;
 
     /**
      * Dump statistics to log.
@@ -80,4 +74,7 @@
      * @param log Logger.
      */
     void dumpStatistics(IgniteLogger log);
+
+    /** Save metadata without exclusive lock on it. */
+    void saveMetadata() throws IgniteInternalCheckedException;
 }
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeList.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeListImpl.java
similarity index 89%
rename from modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeList.java
rename to modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeListImpl.java
index 9e2be89..a498e06 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeList.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/FreeListImpl.java
@@ -30,11 +30,12 @@
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
 import org.apache.ignite.internal.pagememory.PageIdAllocator;
 import org.apache.ignite.internal.pagememory.PageMemory;
 import org.apache.ignite.internal.pagememory.Storable;
 import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
 import org.apache.ignite.internal.pagememory.io.PageIo;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
@@ -49,7 +50,9 @@
 /**
  * Abstract free list.
  */
-public abstract class AbstractFreeList<T extends Storable> extends PagesList implements FreeList<T>, ReuseList {
+public class FreeListImpl extends PagesList implements FreeList, ReuseList {
+    private static final IgniteLogger LOG = Loggers.forClass(FreeListImpl.class);
+
     private static final int BUCKETS = 256; // Must be power of 2.
 
     private static final int REUSE_BUCKET = BUCKETS - 1;
@@ -91,8 +94,9 @@
 
     private final PageHandler<ReuseBag, Long> rmvRow;
 
-    private class WriteRowHandler implements PageHandler<T, Integer> {
-        /** {@inheritDoc} */
+    private final IoStatisticsHolder statHolder;
+
+    private class WriteRowHandler implements PageHandler<Storable, Integer> {
         @Override
         public Integer run(
                 int cacheId,
@@ -100,13 +104,13 @@
                 long page,
                 long pageAddr,
                 PageIo iox,
-                T row,
+                Storable row,
                 int written,
                 IoStatisticsHolder statHolder
         ) throws IgniteInternalCheckedException {
             written = addRow(pageId, pageAddr, iox, row, written);
 
-            putPage(((AbstractDataPageIo<Storable>) iox).getFreeSpace(pageAddr), pageId, pageAddr, statHolder);
+            putPage(((DataPageIo) iox).getFreeSpace(pageAddr), pageId, pageAddr, statHolder);
 
             return written;
         }
@@ -126,10 +130,10 @@
                 long pageId,
                 long pageAddr,
                 PageIo iox,
-                T row,
+                Storable row,
                 int written
         ) throws IgniteInternalCheckedException {
-            AbstractDataPageIo<T> io = (AbstractDataPageIo<T>) iox;
+            DataPageIo io = (DataPageIo) iox;
 
             int rowSize = row.size();
             int oldFreeSpace = io.getFreeSpace(pageAddr);
@@ -162,8 +166,8 @@
         protected int addRowFull(
                 long pageId,
                 long pageAddr,
-                AbstractDataPageIo<T> io,
-                T row,
+                DataPageIo io,
+                Storable row,
                 int rowSize
         ) throws IgniteInternalCheckedException {
             io.addRow(pageId, pageAddr, row, rowSize, pageSize());
@@ -186,8 +190,8 @@
         protected int addRowFragment(
                 long pageId,
                 long pageAddr,
-                AbstractDataPageIo<T> io,
-                T row,
+                DataPageIo io,
+                Storable row,
                 int written,
                 int rowSize
         ) throws IgniteInternalCheckedException {
@@ -220,7 +224,7 @@
         }
     }
 
-    private final class WriteRowsHandler implements PageHandler<CachedIterator<T>, Integer> {
+    private final class WriteRowsHandler implements PageHandler<CachedIterator, Integer> {
         /** {@inheritDoc} */
         @Override
         public Integer run(
@@ -229,15 +233,15 @@
                 long page,
                 long pageAddr,
                 PageIo iox,
-                CachedIterator<T> it,
+                CachedIterator it,
                 int written,
                 IoStatisticsHolder statHolder
         ) throws IgniteInternalCheckedException {
-            AbstractDataPageIo<T> io = (AbstractDataPageIo<T>) iox;
+            DataPageIo io = (DataPageIo) iox;
 
             // Fill the page up to the end.
             while (written != COMPLETE || (!evictionTracker.evictionRequired() && it.hasNext())) {
-                T row = it.get();
+                Storable row = it.get();
 
                 if (written == COMPLETE) {
                     row = it.next();
@@ -290,7 +294,7 @@
                 int itemId,
                 IoStatisticsHolder statHolder
         ) throws IgniteInternalCheckedException {
-            AbstractDataPageIo<T> io = (AbstractDataPageIo<T>) iox;
+            DataPageIo io = (DataPageIo) iox;
 
             int oldFreeSpace = io.getFreeSpace(pageAddr);
 
@@ -337,29 +341,27 @@
      *
      * @param grpId Group ID.
      * @param partId Partition ID.
-     * @param name Structure name (for debug purpose).
      * @param pageMem Page memory.
      * @param reuseList Reuse list or {@code null} if this free list will be a reuse list for itself.
      * @param lockLsnr Page lock listener.
-     * @param log Logger.
      * @param metaPageId Metadata page ID.
      * @param initNew {@code True} if new metadata should be initialized.
      * @param pageListCacheLimit Page list cache limit.
      * @param evictionTracker Page eviction tracker.
      * @throws IgniteInternalCheckedException If failed.
      */
-    public AbstractFreeList(
+    public FreeListImpl(
             int grpId,
             int partId,
             String name,
             PageMemory pageMem,
             @Nullable ReuseList reuseList,
             PageLockListener lockLsnr,
-            IgniteLogger log,
             long metaPageId,
             boolean initNew,
             @Nullable AtomicLong pageListCacheLimit,
-            PageEvictionTracker evictionTracker
+            PageEvictionTracker evictionTracker,
+            IoStatisticsHolder statHolder
     ) throws IgniteInternalCheckedException {
         super(
                 name,
@@ -367,13 +369,14 @@
                 partId,
                 pageMem,
                 lockLsnr,
-                log,
+                LOG,
                 BUCKETS,
                 metaPageId
         );
 
         this.evictionTracker = evictionTracker;
         this.pageListCacheLimit = pageListCacheLimit;
+        this.statHolder = statHolder;
 
         this.reuseList = reuseList == null ? this : reuseList;
 
@@ -388,7 +391,7 @@
         // TODO: https://issues.apache.org/jira/browse/IGNITE-16350
         // TODO: this constant is used because currently we cannot reuse data pages as index pages
         // TODO: and vice-versa. It should be removed when data storage format is finalized.
-        minSizeForDataPage = pageSize - AbstractDataPageIo.MIN_DATA_PAGE_OVERHEAD;
+        minSizeForDataPage = pageSize - DataPageIo.MIN_DATA_PAGE_OVERHEAD;
 
         int shift = 0;
 
@@ -459,7 +462,7 @@
 
         if (dataPages > 0) {
             if (log.isInfoEnabled()) {
-                log.info("FreeList [name={}, buckets={}, dataPages={}, reusePages={}]",
+                log.info("FreeListImpl [name={}, buckets={}, dataPages={}, reusePages={}]",
                         name(), BUCKETS, dataPages, bucketsSize.get(REUSE_BUCKET));
             }
         }
@@ -504,9 +507,8 @@
         return pageMem.allocatePage(grpId, part, FLAG_DATA);
     }
 
-    /** {@inheritDoc} */
     @Override
-    public void insertDataRow(T row, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
+    public void insertDataRow(Storable row) throws IgniteInternalCheckedException {
         int written = 0;
 
         try {
@@ -527,13 +529,12 @@
      * that occupy the whole memory page are written to other pages, and the remainder is added to the current one.
      *
      * @param rows Rows.
-     * @param statHolder Statistics holder to track IO operations.
      * @throws IgniteInternalCheckedException If failed.
      */
     @Override
-    public void insertDataRows(Collection<T> rows, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
+    public void insertDataRows(Collection<? extends Storable> rows) throws IgniteInternalCheckedException {
         try {
-            CachedIterator<T> it = new CachedIterator<>(rows.iterator());
+            CachedIterator it = new CachedIterator(rows.iterator());
 
             int written = COMPLETE;
 
@@ -549,16 +550,16 @@
                     continue;
                 }
 
-                T row = it.get();
+                Storable row = it.get();
 
-                AbstractDataPageIo initIo = null;
+                DataPageIo initIo = null;
 
                 long pageId = takePage(row.size() - written, row, statHolder);
 
                 if (pageId == 0L) {
                     pageId = allocateDataPage(row.partition());
 
-                    initIo = row.ioVersions().latest();
+                    initIo = DataPageIo.VERSIONS.latest();
                 }
 
                 written = write(pageId, writeRowsHnd, initIo, it, written, FAIL_I, statHolder);
@@ -573,12 +574,12 @@
     /**
      * {@link Iterator} implementation that allows to access the current element multiple times.
      */
-    private static class CachedIterator<T> implements Iterator<T> {
-        private final Iterator<T> it;
+    private static class CachedIterator implements Iterator<Storable> {
+        private final Iterator<? extends Storable> it;
 
-        private T next;
+        private Storable next;
 
-        CachedIterator(Iterator<T> it) {
+        CachedIterator(Iterator<? extends Storable> it) {
             this.it = it;
         }
 
@@ -588,13 +589,13 @@
         }
 
         @Override
-        public T next() {
+        public Storable next() {
             next = it.next();
 
             return next;
         }
 
-        T get() {
+        Storable get() {
             return next;
         }
     }
@@ -609,7 +610,7 @@
      *      than the max payload of an empty data page.
      * @throws IgniteInternalCheckedException If failed.
      */
-    private int writeWholePages(T row, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
+    private int writeWholePages(Storable row, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
         assert row.link() == 0 : row.link();
 
         int written = 0;
@@ -631,15 +632,15 @@
      * @return Number of bytes written, {@link #COMPLETE} if the row was fully written.
      * @throws IgniteInternalCheckedException If failed.
      */
-    private int writeSinglePage(T row, int written, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
-        AbstractDataPageIo initIo = null;
+    private int writeSinglePage(Storable row, int written, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
+        DataPageIo initIo = null;
 
         long pageId = takePage(row.size() - written, row, statHolder);
 
         if (pageId == 0L) {
             pageId = allocateDataPage(row.partition());
 
-            initIo = row.ioVersions().latest();
+            initIo = DataPageIo.VERSIONS.latest();
         }
 
         written = write(pageId, writeRowHnd, initIo, row, written, FAIL_I, statHolder);
@@ -658,12 +659,12 @@
      * @return Page identifier or {@code 0} if no page found in free list.
      * @throws IgniteInternalCheckedException If failed.
      */
-    private long takePage(int size, T row, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
+    private long takePage(int size, Storable row, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
         long pageId = 0;
 
         if (size < minSizeForDataPage) {
             for (int b = bucket(size, false) + 1; b < REUSE_BUCKET; b++) {
-                pageId = takeEmptyPage(b, row.ioVersions(), statHolder);
+                pageId = takeEmptyPage(b, DataPageIo.VERSIONS, statHolder);
 
                 if (pageId != 0L) {
                     break;
@@ -673,12 +674,12 @@
 
         if (pageId == 0L) { // Handle reuse bucket.
             if (reuseList == this) {
-                pageId = takeEmptyPage(REUSE_BUCKET, row.ioVersions(), statHolder);
+                pageId = takeEmptyPage(REUSE_BUCKET, DataPageIo.VERSIONS, statHolder);
             } else {
                 pageId = reuseList.takeRecycledPage();
 
                 if (pageId != 0) {
-                    pageId = reuseList.initRecycledPage(pageId, FLAG_DATA, row.ioVersions().latest());
+                    pageId = reuseList.initRecycledPage(pageId, FLAG_DATA, DataPageIo.VERSIONS.latest());
                 }
             }
         }
@@ -688,7 +689,7 @@
         }
 
         assert PageIdUtils.flag(pageId) == FLAG_DATA
-                : "rowVersions=" + row.ioVersions() + ", pageId=" + PageIdUtils.toDetailString(pageId);
+                : "rowVersions=" + DataPageIo.VERSIONS + ", pageId=" + PageIdUtils.toDetailString(pageId);
 
         return PageIdUtils.changePartitionId(pageId, row.partition());
     }
@@ -703,7 +704,7 @@
      * @return Prepared page id.
      * @see PagesList#initReusedPage(long, long, int, byte, PageIo)
      */
-    private long initReusedPage(T row, long reusedPageId, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
+    private long initReusedPage(Storable row, long reusedPageId, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
         long reusedPage = acquirePage(reusedPageId, statHolder);
         try {
             long reusedPageAddr = writeLock(reusedPageId, reusedPage);
@@ -716,7 +717,7 @@
                         reusedPageAddr,
                         row.partition(),
                         FLAG_DATA,
-                        row.ioVersions().latest()
+                        DataPageIo.VERSIONS.latest()
                 );
             } finally {
                 writeUnlock(reusedPageId, reusedPage, reusedPageAddr, true);
@@ -731,8 +732,7 @@
     public <S, R> R updateDataRow(
             long link,
             PageHandler<S, R> pageHnd,
-            S arg,
-            IoStatisticsHolder statHolder
+            S arg
     ) throws IgniteInternalCheckedException {
         assert link != 0;
 
@@ -754,9 +754,8 @@
         }
     }
 
-    /** {@inheritDoc} */
     @Override
-    public void removeDataRowByLink(long link, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
+    public void removeDataRowByLink(long link) throws IgniteInternalCheckedException {
         assert link != 0;
 
         try {
@@ -886,9 +885,13 @@
         }
     }
 
-    /** {@inheritDoc} */
+    @Override
+    public void saveMetadata() throws IgniteInternalCheckedException {
+        saveMetadata(statHolder);
+    }
+
     @Override
     public String toString() {
-        return "FreeList [name=" + name() + ']';
+        return "FreeListImpl [name=" + name() + ']';
     }
 }
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/PagesList.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/PagesList.java
index 5b3c773..0979df8 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/PagesList.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/freelist/PagesList.java
@@ -48,11 +48,10 @@
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.pagememory.PageIdAllocator;
 import org.apache.ignite.internal.pagememory.PageMemory;
-import org.apache.ignite.internal.pagememory.Storable;
 import org.apache.ignite.internal.pagememory.datastructure.DataStructure;
 import org.apache.ignite.internal.pagememory.freelist.io.PagesListMetaIo;
 import org.apache.ignite.internal.pagememory.freelist.io.PagesListNodeIo;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
 import org.apache.ignite.internal.pagememory.io.IoVersions;
 import org.apache.ignite.internal.pagememory.io.PageIo;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
@@ -160,7 +159,7 @@
             decrementBucketSize(oldBucket);
 
             // Recalculate bucket because page free space can be changed concurrently.
-            int freeSpace = ((AbstractDataPageIo<Storable>) iox).getFreeSpace(pageAddr);
+            int freeSpace = ((DataPageIo) iox).getFreeSpace(pageAddr);
 
             int newBucket = getBucketIndex(freeSpace);
 
@@ -919,7 +918,7 @@
         } else {
             incrementBucketSize(bucket);
 
-            AbstractDataPageIo dataIo = pageMem.ioRegistry().resolve(dataAddr);
+            DataPageIo dataIo = pageMem.ioRegistry().resolve(dataAddr);
             dataIo.setFreeListPageId(dataAddr, pageId);
         }
 
@@ -945,7 +944,7 @@
         if (pagesCache.add(dataId)) {
             incrementBucketSize(bucket);
 
-            AbstractDataPageIo dataIo = pageMem.ioRegistry().resolve(dataAddr);
+            DataPageIo dataIo = pageMem.ioRegistry().resolve(dataAddr);
 
             if (dataIo.getFreeListPageId(dataAddr) != 0L) {
                 dataIo.setFreeListPageId(dataAddr, 0L);
@@ -980,7 +979,7 @@
             int bucket,
             IoStatisticsHolder statHolder
     ) throws IgniteInternalCheckedException {
-        AbstractDataPageIo dataIo = pageMem.ioRegistry().resolve(dataAddr);
+        DataPageIo dataIo = pageMem.ioRegistry().resolve(dataAddr);
 
         // Attempt to add page failed: the node page is full.
         if (isReuseBucket(bucket)) {
@@ -1445,7 +1444,7 @@
     protected final boolean removeDataPage(
             final long dataId,
             final long dataAddr,
-            AbstractDataPageIo dataIo,
+            DataPageIo dataIo,
             int bucket,
             IoStatisticsHolder statHolder
     ) throws IgniteInternalCheckedException {
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/AbstractDataPageIo.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/DataPageIo.java
similarity index 92%
rename from modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/AbstractDataPageIo.java
rename to modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/DataPageIo.java
index 3cc6f11..c8be9bf 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/AbstractDataPageIo.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/io/DataPageIo.java
@@ -20,11 +20,9 @@
 import static org.apache.ignite.internal.pagememory.PageIdAllocator.FLAG_DATA;
 
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
-import java.util.List;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.lang.IgniteStringBuilder;
 import org.apache.ignite.internal.pagememory.PageMemory;
@@ -158,7 +156,10 @@
  *     +-----------------------------------------------------------------------+
  * </pre>
  */
-public abstract class AbstractDataPageIo<T extends Storable> extends PageIo {
+public class DataPageIo extends PageIo {
+    /** Data page IO type. */
+    private static final short T_DATA_PAGE_IO = 1000;
+
     private static final int SHOW_ITEM = 0b0001;
 
     private static final int SHOW_PAYLOAD_LEN = 0b0010;
@@ -190,17 +191,20 @@
 
     /** Special index value that signals that the given itemId does not exist. */
     private static final int NON_EXISTENT_ITEM_INDEX = -1;
+
     /** Special offset value that signals that the given itemId does not exist. */
     private static final int NON_EXISTENT_ITEM_OFFSET = -1;
 
+    /** I/O versions. */
+    public static final IoVersions<DataPageIo> VERSIONS = new IoVersions<>(new DataPageIo(1));
+
     /**
      * Constructor.
      *
-     * @param type Page type.
      * @param ver Page format version.
      */
-    protected AbstractDataPageIo(int type, int ver) {
-        super(type, ver, FLAG_DATA);
+    protected DataPageIo(int ver) {
+        super(T_DATA_PAGE_IO, ver, FLAG_DATA);
     }
 
     /** {@inheritDoc} */
@@ -394,42 +398,6 @@
     }
 
     /**
-     * Returns rows number in the given data page.
-     *
-     * @param pageAddr Page address.
-     */
-    public int getRowsCount(long pageAddr) {
-        return getDirectCount(pageAddr);
-    }
-
-    /**
-     * Applies closure to all items in the page.
-     *
-     * @param pageAddr Page address.
-     * @param c Closure.
-     * @param <U> Closure return type.
-     * @return Collection of closure results for all items in page.
-     * @throws IgniteInternalCheckedException In case of error in closure body.
-     */
-    public <U> List<U> forAllItems(long pageAddr, Closure<U> c) throws IgniteInternalCheckedException {
-        assertPageType(pageAddr);
-
-        long pageId = getPageId(pageAddr);
-
-        int cnt = getDirectCount(pageAddr);
-
-        List<U> res = new ArrayList<>(cnt);
-
-        for (int i = 0; i < cnt; i++) {
-            long link = PageIdUtils.link(pageId, i);
-
-            res.add(c.apply(link));
-        }
-
-        return res;
-    }
-
-    /**
      * Writes inderect count.
      *
      * @param pageAddr Page address.
@@ -1033,15 +1001,14 @@
      * @param row Data row.
      * @param rowSize Row size.
      * @param pageSize Page size.
-     * @throws IgniteInternalCheckedException If failed.
      */
     public void addRow(
             final long pageId,
             final long pageAddr,
-            T row,
+            Storable row,
             final int rowSize,
             final int pageSize
-    ) throws IgniteInternalCheckedException {
+    ) {
         assert rowSize <= getFreeSpace(pageAddr) : "can't call addRow if not enough space for the whole row";
         assert rowSize <= 0xFFFF : "Row size is too big: " + rowSize;
         assertPageType(pageAddr);
@@ -1053,7 +1020,7 @@
 
         int dataOff = getDataOffsetForWrite(pageAddr, fullEntrySize, directCnt, indirectCnt, pageSize);
 
-        writeRowData(pageAddr, dataOff, rowSize, row, true);
+        row.writeRowData(pageAddr, dataOff, rowSize, true);
 
         int itemId = addItem(pageAddr, fullEntrySize, directCnt, indirectCnt, dataOff, pageSize);
 
@@ -1160,7 +1127,7 @@
             PageMemory pageMem,
             long pageId,
             long pageAddr,
-            T row,
+            Storable row,
             int written,
             int rowSize,
             int pageSize
@@ -1192,7 +1159,7 @@
 
         int rowOff = rowSize - written - payloadSize;
 
-        writeFragmentData(row, buf, rowOff, payloadSize);
+        row.writeFragmentData(buf, rowOff, payloadSize);
 
         int itemId = addItem(pageAddr, fullEntrySize, directCnt, indirectCnt, dataOff, pageSize);
 
@@ -1210,48 +1177,11 @@
      * @param pageId Page ID.
      * @param itemId Item ID.
      */
-    private void setLinkByPageId(T row, long pageId, int itemId) {
+    private void setLinkByPageId(Storable row, long pageId, int itemId) {
         row.link(PageIdUtils.link(pageId, itemId));
     }
 
     /**
-     * Writes row data fragment.
-     *
-     * @param row Row.
-     * @param pageBuf Byte buffer.
-     * @param rowOff Offset in row data bytes.
-     * @param payloadSize Data length that should be written in a fragment.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    protected abstract void writeFragmentData(
-            final T row,
-            final ByteBuffer pageBuf,
-            final int rowOff,
-            final int payloadSize
-    ) throws IgniteInternalCheckedException;
-
-    /**
-     * Writes a content of a byte buffer into a page.
-     *
-     * @param pageBuffer Direct page buffer.
-     * @param valueBuffer Byte buffer with value bytes.
-     * @param offset Offset within the value buffer.
-     * @param payloadSize Number of bytes to write.
-     */
-    protected void putValueBufferIntoPage(ByteBuffer pageBuffer, ByteBuffer valueBuffer, int offset, int payloadSize) {
-        int oldPosition = valueBuffer.position();
-        int oldLimit = valueBuffer.limit();
-
-        valueBuffer.position(offset);
-        valueBuffer.limit(offset + payloadSize);
-
-        pageBuffer.put(valueBuffer);
-
-        valueBuffer.position(oldPosition);
-        valueBuffer.limit(oldLimit);
-    }
-
-    /**
      * Inserts an item.
      *
      * @param pageAddr Page address.
@@ -1420,37 +1350,10 @@
         PageUtils.copyMemory(addr, off, addr, off + step, cnt);
     }
 
-    /**
-     * Writes a row.
-     *
-     * @param pageAddr Page address.
-     * @param dataOff Data offset.
-     * @param payloadSize Payload size.
-     * @param row Data row.
-     * @param newRow {@code False} if existing cache entry is updated, in this case skip key data write.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    protected abstract void writeRowData(
-            long pageAddr,
-            int dataOff,
-            int payloadSize,
-            T row,
-            boolean newRow
-    ) throws IgniteInternalCheckedException;
-
-    /**
-     * Defines closure interface for applying computations to data page items.
-     *
-     * @param <T> Closure return type.
-     */
-    public interface Closure<T> {
-        /**
-         * Closure body.
-         *
-         * @param link Link to item.
-         * @return Closure return value.
-         * @throws IgniteInternalCheckedException In case of error in closure body.
-         */
-        T apply(long link) throws IgniteInternalCheckedException;
+    @Override
+    protected void printPage(long addr, int pageSize, IgniteStringBuilder sb) {
+        sb.app("DataPageIo [\n");
+        printPageLayout(addr, pageSize, sb);
+        sb.app("\n]");
     }
 }
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java
index e752116..80b1ab6 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/PartitionMeta.java
@@ -54,9 +54,7 @@
 
     private volatile long lastReplicationProtocolGroupConfigFirstPageId;
 
-    private volatile long rowVersionFreeListRootPageId;
-
-    private volatile long indexColumnsFreeListRootPageId;
+    private volatile long freeListRootPageId;
 
     private volatile long versionChainTreeRootPageId;
 
@@ -81,7 +79,7 @@
      *
      * @param checkpointId Checkpoint ID.
      * @param lastAppliedIndex Last applied index value.
-     * @param rowVersionFreeListRootPageId Row version free list root page ID.
+     * @param freeListRootPageId Free list root page ID.
      * @param versionChainTreeRootPageId Version chain tree root page ID.
      * @param pageCount Count of pages in the partition.
      */
@@ -90,8 +88,7 @@
             long lastAppliedIndex,
             long lastAppliedTerm,
             long lastReplicationProtocolGroupConfigFirstPageId,
-            long rowVersionFreeListRootPageId,
-            long indexColumnsFreeListRootPageId,
+            long freeListRootPageId,
             long versionChainTreeRootPageId,
             long indexTreeMetaPageId,
             long gcQueueMetaPageId,
@@ -101,8 +98,7 @@
         this.lastAppliedIndex = lastAppliedIndex;
         this.lastAppliedTerm = lastAppliedTerm;
         this.lastReplicationProtocolGroupConfigFirstPageId = lastReplicationProtocolGroupConfigFirstPageId;
-        this.rowVersionFreeListRootPageId = rowVersionFreeListRootPageId;
-        this.indexColumnsFreeListRootPageId = indexColumnsFreeListRootPageId;
+        this.freeListRootPageId = freeListRootPageId;
         this.versionChainTreeRootPageId = versionChainTreeRootPageId;
         this.indexTreeMetaPageId = indexTreeMetaPageId;
         this.gcQueueMetaPageId = gcQueueMetaPageId;
@@ -125,8 +121,7 @@
                 metaIo.getLastAppliedIndex(pageAddr),
                 metaIo.getLastAppliedTerm(pageAddr),
                 metaIo.getLastReplicationProtocolGroupConfigFirstPageId(pageAddr),
-                metaIo.getRowVersionFreeListRootPageId(pageAddr),
-                metaIo.getIndexColumnsFreeListRootPageId(pageAddr),
+                PartitionMetaIo.getFreeListRootPageId(pageAddr),
                 metaIo.getVersionChainTreeRootPageId(pageAddr),
                 metaIo.getIndexTreeMetaPageId(pageAddr),
                 metaIo.getGcQueueMetaPageId(pageAddr),
@@ -202,41 +197,22 @@
     }
 
     /**
-     * Returns row version free list root page ID.
+     * Returns free list root page ID.
      */
-    public long rowVersionFreeListRootPageId() {
-        return rowVersionFreeListRootPageId;
+    public long freeListRootPageId() {
+        return freeListRootPageId;
     }
 
     /**
-     * Sets row version free list root page ID.
+     * Sets free list root page ID.
      *
      * @param checkpointId Checkpoint ID.
-     * @param rowVersionFreeListRootPageId Row version free list root page ID.
+     * @param freeListRootPageId Free list root page ID.
      */
-    public void rowVersionFreeListRootPageId(@Nullable UUID checkpointId, long rowVersionFreeListRootPageId) {
+    public void freeListRootPageId(@Nullable UUID checkpointId, long freeListRootPageId) {
         updateSnapshot(checkpointId);
 
-        this.rowVersionFreeListRootPageId = rowVersionFreeListRootPageId;
-    }
-
-    /**
-     * Returns index columns free list root page id.
-     */
-    public long indexColumnsFreeListRootPageId() {
-        return indexColumnsFreeListRootPageId;
-    }
-
-    /**
-     * Sets an index columns free list root page id.
-     *
-     * @param checkpointId Checkpoint id.
-     * @param indexColumnsFreeListRootPageId Index columns free list root page id.
-     */
-    public void indexColumnsFreeListRootPageId(@Nullable UUID checkpointId, long indexColumnsFreeListRootPageId) {
-        updateSnapshot(checkpointId);
-
-        this.indexColumnsFreeListRootPageId = indexColumnsFreeListRootPageId;
+        this.freeListRootPageId = freeListRootPageId;
     }
 
     /**
@@ -362,9 +338,7 @@
 
         private final long versionChainTreeRootPageId;
 
-        private final long rowVersionFreeListRootPageId;
-
-        private final long indexColumnsFreeListRootPageId;
+        private final long freeListRootPageId;
 
         private final long indexTreeMetaPageId;
 
@@ -386,8 +360,7 @@
             lastAppliedTerm = partitionMeta.lastAppliedTerm;
             lastReplicationProtocolGroupConfigFirstPageId = partitionMeta.lastReplicationProtocolGroupConfigFirstPageId;
             versionChainTreeRootPageId = partitionMeta.versionChainTreeRootPageId;
-            rowVersionFreeListRootPageId = partitionMeta.rowVersionFreeListRootPageId;
-            indexColumnsFreeListRootPageId = partitionMeta.indexColumnsFreeListRootPageId;
+            freeListRootPageId = partitionMeta.freeListRootPageId;
             indexTreeMetaPageId = partitionMeta.indexTreeMetaPageId;
             gcQueueMetaPageId = partitionMeta.gcQueueMetaPageId;
             pageCount = partitionMeta.pageCount;
@@ -423,17 +396,10 @@
         }
 
         /**
-         * Returns row version free list root page ID.
+         * Returns free list root page ID.
          */
-        public long rowVersionFreeListRootPageId() {
-            return rowVersionFreeListRootPageId;
-        }
-
-        /**
-         * Returns index columns free list root page ID.
-         */
-        public long indexColumnsFreeListRootPageId() {
-            return indexColumnsFreeListRootPageId;
+        public long freeListRootPageId() {
+            return freeListRootPageId;
         }
 
         /**
@@ -477,8 +443,7 @@
             metaIo.setLastAppliedTerm(pageAddr, lastAppliedTerm);
             metaIo.setLastReplicationProtocolGroupConfigFirstPageId(pageAddr, lastReplicationProtocolGroupConfigFirstPageId);
             metaIo.setVersionChainTreeRootPageId(pageAddr, versionChainTreeRootPageId);
-            metaIo.setIndexColumnsFreeListRootPageId(pageAddr, indexColumnsFreeListRootPageId);
-            metaIo.setRowVersionFreeListRootPageId(pageAddr, rowVersionFreeListRootPageId);
+            metaIo.setFreeListRootPageId(pageAddr, freeListRootPageId);
             metaIo.setIndexTreeMetaPageId(pageAddr, indexTreeMetaPageId);
             metaIo.setGcQueueMetaPageId(pageAddr, gcQueueMetaPageId);
             metaIo.setPageCount(pageAddr, pageCount);
diff --git a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java
index 6c64a5d..f038d2c 100644
--- a/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java
+++ b/modules/page-memory/src/main/java/org/apache/ignite/internal/pagememory/persistence/io/PartitionMetaIo.java
@@ -38,11 +38,9 @@
 
     private static final int LAST_REPLICATION_PROTOCOL_GROUP_CONFIG_FIRST_PAGE_ID_OFF = LAST_APPLIED_TERM_OFF + Long.BYTES;
 
-    private static final int ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF = LAST_REPLICATION_PROTOCOL_GROUP_CONFIG_FIRST_PAGE_ID_OFF + Long.BYTES;
+    private static final int FREE_LIST_ROOT_PAGE_ID_OFF = LAST_REPLICATION_PROTOCOL_GROUP_CONFIG_FIRST_PAGE_ID_OFF + Long.BYTES;
 
-    private static final int INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF = ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF + Long.BYTES;
-
-    private static final int VERSION_CHAIN_TREE_ROOT_PAGE_ID_OFF = INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF + Long.BYTES;
+    private static final int VERSION_CHAIN_TREE_ROOT_PAGE_ID_OFF = FREE_LIST_ROOT_PAGE_ID_OFF + Long.BYTES;
 
     public static final int INDEX_TREE_META_PAGE_ID_OFF = VERSION_CHAIN_TREE_ROOT_PAGE_ID_OFF + Long.BYTES;
 
@@ -75,8 +73,7 @@
         setLastAppliedIndex(pageAddr, 0);
         setLastAppliedTerm(pageAddr, 0);
         setLastReplicationProtocolGroupConfigFirstPageId(pageAddr, 0);
-        setRowVersionFreeListRootPageId(pageAddr, 0);
-        setIndexColumnsFreeListRootPageId(pageAddr, 0);
+        setFreeListRootPageId(pageAddr, 0);
         setVersionChainTreeRootPageId(pageAddr, 0);
         setIndexTreeMetaPageId(pageAddr, 0);
         setGcQueueMetaPageId(pageAddr, 0);
@@ -148,45 +145,24 @@
     }
 
     /**
-     * Sets row version free list root page ID.
+     * Sets free list root page ID.
      *
      * @param pageAddr Page address.
-     * @param pageId Row version free list root page ID.
+     * @param pageId Free list root page ID.
      */
-    public void setRowVersionFreeListRootPageId(long pageAddr, long pageId) {
+    public void setFreeListRootPageId(long pageAddr, long pageId) {
         assertPageType(pageAddr);
 
-        putLong(pageAddr, ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF, pageId);
+        putLong(pageAddr, FREE_LIST_ROOT_PAGE_ID_OFF, pageId);
     }
 
     /**
-     * Returns row version free list root page ID.
+     * Returns free list root page ID.
      *
      * @param pageAddr Page address.
      */
-    public long getRowVersionFreeListRootPageId(long pageAddr) {
-        return getLong(pageAddr, ROW_VERSION_FREE_LIST_ROOT_PAGE_ID_OFF);
-    }
-
-    /**
-     * Sets an index columns free list root page id.
-     *
-     * @param pageAddr Page address.
-     * @param pageId Root page id.
-     */
-    public void setIndexColumnsFreeListRootPageId(long pageAddr, long pageId) {
-        assertPageType(pageAddr);
-
-        putLong(pageAddr, INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF, pageId);
-    }
-
-    /**
-     * Returns an index columns free list root page id.
-     *
-     * @param pageAddr Page address.
-     */
-    public long getIndexColumnsFreeListRootPageId(long pageAddr) {
-        return getLong(pageAddr, INDEX_COLUMNS_FREE_LIST_ROOT_PAGE_ID_OFF);
+    public static long getFreeListRootPageId(long pageAddr) {
+        return getLong(pageAddr, FREE_LIST_ROOT_PAGE_ID_OFF);
     }
 
     /**
@@ -302,8 +278,7 @@
                 .app("lastAppliedIndex=").app(getLastAppliedIndex(addr)).nl()
                 .app("lastAppliedTerm=").app(getLastAppliedTerm(addr)).nl()
                 .app("lastReplicationProtocolGroupConfigFirstPageId=").app(getLastReplicationProtocolGroupConfigFirstPageId(addr)).nl()
-                .app("rowVersionFreeListRootPageId=").appendHex(getRowVersionFreeListRootPageId(addr)).nl()
-                .app("indexColumnsFreeListRootPageId(=").appendHex(getIndexColumnsFreeListRootPageId(addr)).nl()
+                .app("freeListRootPageId=").appendHex(getFreeListRootPageId(addr)).nl()
                 .app("versionChainTreeRootPageId=").appendHex(getVersionChainTreeRootPageId(addr)).nl()
                 .app("indexTreeMetaPageId=").appendHex(getIndexTreeMetaPageId(addr)).nl()
                 .app("gcQueueMetaPageId=").appendHex(getGcQueueMetaPageId(addr)).nl()
diff --git a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeListTest.java b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/FreeListImplTest.java
similarity index 87%
rename from modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeListTest.java
rename to modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/FreeListImplTest.java
index 21c71cf..98cc937 100644
--- a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/AbstractFreeListTest.java
+++ b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/FreeListImplTest.java
@@ -28,9 +28,9 @@
 import static org.junit.jupiter.api.Assertions.assertNull;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.ConcurrentHashMap;
@@ -42,13 +42,14 @@
 import org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.pagememory.PageMemory;
+import org.apache.ignite.internal.pagememory.Storable;
 import org.apache.ignite.internal.pagememory.TestPageIoRegistry;
 import org.apache.ignite.internal.pagememory.configuration.schema.VolatilePageMemoryProfileChange;
 import org.apache.ignite.internal.pagememory.configuration.schema.VolatilePageMemoryProfileConfiguration;
 import org.apache.ignite.internal.pagememory.configuration.schema.VolatilePageMemoryProfileConfigurationSchema;
 import org.apache.ignite.internal.pagememory.evict.PageEvictionTrackerNoOp;
 import org.apache.ignite.internal.pagememory.inmemory.VolatilePageMemory;
-import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
 import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.storage.configurations.StorageProfileConfiguration;
@@ -61,10 +62,10 @@
 import org.junit.jupiter.params.provider.MethodSource;
 
 /**
- * Class to test the {@link AbstractFreeList}.
+ * Class to test the {@link FreeListImpl}.
  */
 @ExtendWith(ConfigurationExtension.class)
-public class AbstractFreeListTest extends BaseIgniteAbstractTest {
+public class FreeListImplTest extends BaseIgniteAbstractTest {
     private static final long MAX_SIZE = 1024 * MiB;
 
     private static final int BATCH_SIZE = 100;
@@ -85,7 +86,7 @@
     @ParameterizedTest
     @MethodSource("provideTestArguments")
     void testSingleTread(int pageSize, boolean batched) throws Exception {
-        FreeList<TestDataRow> freeList = createFreeList(pageSize);
+        FreeList freeList = createFreeList(pageSize);
 
         Map<Long, TestDataRow> stored = new HashMap<>();
 
@@ -97,7 +98,7 @@
     @ParameterizedTest
     @MethodSource("provideTestArguments")
     void testMultiThread(int pageSize, boolean batched) throws Exception {
-        FreeList<TestDataRow> freeList = createFreeList(pageSize);
+        FreeList freeList = createFreeList(pageSize);
 
         Map<Long, TestDataRow> stored = new ConcurrentHashMap<>();
 
@@ -135,30 +136,29 @@
         );
     }
 
-    private FreeList<TestDataRow> createFreeList(int pageSize) throws Exception {
+    private FreeList createFreeList(int pageSize) throws Exception {
         pageMemory = createPageMemory(pageSize);
 
         pageMemory.start();
 
         long metaPageId = pageMemory.allocatePage(1, 1, FLAG_DATA);
 
-        return new AbstractFreeList<>(
+        return new FreeListImpl(
                 0,
                 1,
-                "freelist",
+                "TestFreeList",
                 pageMemory,
                 null,
                 PageLockListenerNoOp.INSTANCE,
-                log,
                 metaPageId,
                 true,
                 null,
-                PageEvictionTrackerNoOp.INSTANCE
+                PageEvictionTrackerNoOp.INSTANCE,
+                IoStatisticsHolderNoOp.INSTANCE
         ) {
-            /** {@inheritDoc} */
             @Override
-            public void insertDataRow(TestDataRow row, IoStatisticsHolder statHolder) throws IgniteInternalCheckedException {
-                super.insertDataRow(row, statHolder);
+            public void insertDataRow(Storable row) throws IgniteInternalCheckedException {
+                super.insertDataRow(row);
 
                 assertEquals(row.partition(), partitionId(row.link()));
             }
@@ -176,7 +176,7 @@
 
         ioRegistry.loadFromServiceLoader();
 
-        ioRegistry.load(TestDataPageIo.VERSIONS);
+        ioRegistry.load(DataPageIo.VERSIONS);
 
         return new VolatilePageMemory(
                 (VolatilePageMemoryProfileConfiguration) fixConfiguration(storageProfileCfg),
@@ -185,7 +185,7 @@
         );
     }
 
-    private void prepare(FreeList<TestDataRow> freeList, Map<Long, TestDataRow> stored) throws Exception {
+    private void prepare(FreeList freeList, Map<Long, TestDataRow> stored) throws Exception {
         Random rnd = ThreadLocalRandom.current();
 
         for (int i = 0; i < 100; i++) {
@@ -193,7 +193,7 @@
 
             TestDataRow row = new TestDataRow(size);
 
-            freeList.insertDataRow(row, IoStatisticsHolderNoOp.INSTANCE);
+            freeList.insertDataRow(row);
 
             assertNotEquals(0L, row.link());
 
@@ -204,12 +204,12 @@
     }
 
     private void insertDeleteRows(
-            FreeList<TestDataRow> freeList,
+            FreeList freeList,
             Map<Long, TestDataRow> stored,
             AtomicBoolean grow,
             boolean batched
     ) throws Exception {
-        List<TestDataRow> rows = new ArrayList<>(BATCH_SIZE);
+        Collection<TestDataRow> rows = new ArrayList<>(BATCH_SIZE);
 
         Random rnd = ThreadLocalRandom.current();
 
@@ -245,7 +245,7 @@
                     rows.add(row);
 
                     if (rows.size() == BATCH_SIZE) {
-                        freeList.insertDataRows(rows, IoStatisticsHolderNoOp.INSTANCE);
+                        freeList.insertDataRows(rows);
 
                         for (TestDataRow row0 : rows) {
                             assertNotEquals(0L, row0.link());
@@ -261,7 +261,7 @@
                     continue;
                 }
 
-                freeList.insertDataRow(row, IoStatisticsHolderNoOp.INSTANCE);
+                freeList.insertDataRow(row);
 
                 assertNotEquals(0L, row.link());
 
@@ -278,7 +278,7 @@
                         TestDataRow rmvd = stored.remove(row.link());
 
                         if (rmvd != null) {
-                            freeList.removeDataRowByLink(row.link(), IoStatisticsHolderNoOp.INSTANCE);
+                            freeList.removeDataRowByLink(row.link());
 
                             break;
                         }
diff --git a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/TestDataPageIo.java b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/TestDataPageIo.java
deleted file mode 100644
index 8893e7d..0000000
--- a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/TestDataPageIo.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.pagememory.freelist;
-
-import java.nio.ByteBuffer;
-import org.apache.ignite.internal.lang.IgniteStringBuilder;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
-import org.apache.ignite.internal.pagememory.io.IoVersions;
-import org.apache.ignite.internal.pagememory.util.PageUtils;
-
-/**
- * Test DataPageIo for {@link TestDataRow}.
- */
-class TestDataPageIo extends AbstractDataPageIo<TestDataRow> {
-    /** I/O versions. */
-    static final IoVersions<TestDataPageIo> VERSIONS = new IoVersions<>(new TestDataPageIo());
-
-    /**
-     * Private constructor.
-     */
-    private TestDataPageIo() {
-        super(1, 1);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void writeRowData(long pageAddr, int dataOff, int payloadSize, TestDataRow row, boolean newRow) {
-        assertPageType(pageAddr);
-
-        long addr = pageAddr + dataOff;
-
-        if (newRow) {
-            PageUtils.putShort(addr, 0, (short) payloadSize);
-
-            addr += 2;
-        } else {
-            addr += 2;
-        }
-
-        PageUtils.putBytes(addr, 0, row.bytes);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void writeFragmentData(TestDataRow row, ByteBuffer pageBuf, int rowOff, int payloadSize) {
-        assertPageType(pageBuf);
-
-        if (payloadSize > 0) {
-            pageBuf.put(row.bytes, rowOff, payloadSize);
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void printPage(long addr, int pageSize, IgniteStringBuilder sb) {
-        sb.app("TestDataPageIo [\n");
-        printPageLayout(addr, pageSize, sb);
-        sb.app("\n]");
-    }
-}
diff --git a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/TestDataRow.java b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/TestDataRow.java
index 8ee6b35..6fec70d 100644
--- a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/TestDataRow.java
+++ b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/freelist/TestDataRow.java
@@ -17,11 +17,10 @@
 
 package org.apache.ignite.internal.pagememory.freelist;
 
-import static org.apache.ignite.internal.pagememory.freelist.TestDataPageIo.VERSIONS;
 
+import java.nio.ByteBuffer;
 import org.apache.ignite.internal.pagememory.Storable;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
-import org.apache.ignite.internal.pagememory.io.IoVersions;
+import org.apache.ignite.internal.pagememory.util.PageUtils;
 
 /**
  * Test storable row with raw data.
@@ -70,9 +69,25 @@
         return 0;
     }
 
-    /** {@inheritDoc} */
     @Override
-    public IoVersions<? extends AbstractDataPageIo<?>> ioVersions() {
-        return VERSIONS;
+    public void writeRowData(long pageAddr, int dataOff, int payloadSize, boolean newRow) {
+        long addr = pageAddr + dataOff;
+
+        if (newRow) {
+            PageUtils.putShort(addr, 0, (short) payloadSize);
+
+            addr += 2;
+        } else {
+            addr += 2;
+        }
+
+        PageUtils.putBytes(addr, 0, bytes);
+    }
+
+    @Override
+    public void writeFragmentData(ByteBuffer pageBuf, int rowOff, int payloadSize) {
+        if (payloadSize > 0) {
+            pageBuf.put(bytes, rowOff, payloadSize);
+        }
     }
 }
diff --git a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java
index 415cfc0..87fc654 100644
--- a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java
+++ b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaManagerTest.java
@@ -105,7 +105,7 @@
                 assertEquals(0, meta.lastAppliedTerm());
                 assertEquals(0, meta.lastReplicationProtocolGroupConfigFirstPageId());
                 assertEquals(0, meta.versionChainTreeRootPageId());
-                assertEquals(0, meta.rowVersionFreeListRootPageId());
+                assertEquals(0, meta.freeListRootPageId());
                 assertEquals(1, meta.pageCount());
                 assertEquals(HybridTimestamp.MIN_VALUE.longValue(), meta.leaseStartTime());
 
@@ -113,7 +113,7 @@
                 meta.lastApplied(null, 50, 10);
                 meta.lastReplicationProtocolGroupConfigFirstPageId(null, 12);
                 meta.versionChainTreeRootPageId(null, 300);
-                meta.rowVersionFreeListRootPageId(null, 900);
+                meta.freeListRootPageId(null, 900);
                 meta.incrementPageCount(null);
                 meta.updateLease(null, 500);
 
@@ -134,7 +134,7 @@
                 assertEquals(10, meta.lastAppliedTerm());
                 assertEquals(12, meta.lastReplicationProtocolGroupConfigFirstPageId());
                 assertEquals(300, meta.versionChainTreeRootPageId());
-                assertEquals(900, meta.rowVersionFreeListRootPageId());
+                assertEquals(900, meta.freeListRootPageId());
                 assertEquals(2, meta.pageCount());
                 assertEquals(500, meta.leaseStartTime());
             }
@@ -143,7 +143,7 @@
             try (FilePageStore filePageStore = createFilePageStore(testFilePath)) {
                 manager.writeMetaToBuffer(
                         partId,
-                        new PartitionMeta(UUID.randomUUID(), 100, 10, 34, 900, 500, 300, 200, 400, 4, 1000).metaSnapshot(null),
+                        new PartitionMeta(UUID.randomUUID(), 100, 10, 34, 900, 300, 200, 400, 4, 1000).metaSnapshot(null),
                         buffer.rewind()
                 );
 
@@ -161,8 +161,7 @@
                 assertEquals(100, meta.lastAppliedIndex());
                 assertEquals(10, meta.lastAppliedTerm());
                 assertEquals(34, meta.lastReplicationProtocolGroupConfigFirstPageId());
-                assertEquals(900, meta.rowVersionFreeListRootPageId());
-                assertEquals(500, meta.indexColumnsFreeListRootPageId());
+                assertEquals(900, meta.freeListRootPageId());
                 assertEquals(300, meta.versionChainTreeRootPageId());
                 assertEquals(200, meta.indexTreeMetaPageId());
                 assertEquals(400, meta.gcQueueMetaPageId());
@@ -185,7 +184,7 @@
                 assertEquals(0, meta.lastAppliedTerm());
                 assertEquals(0, meta.lastReplicationProtocolGroupConfigFirstPageId());
                 assertEquals(0, meta.versionChainTreeRootPageId());
-                assertEquals(0, meta.rowVersionFreeListRootPageId());
+                assertEquals(0, meta.freeListRootPageId());
                 assertEquals(1, meta.pageCount());
             }
         } finally {
diff --git a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java
index 6d252df..9c8b9ec 100644
--- a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java
+++ b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/PartitionMetaTest.java
@@ -112,25 +112,25 @@
     }
 
     @Test
-    void testRowVersionFreeListRootPageId() {
+    void testFreeListRootPageId() {
         PartitionMeta meta = new PartitionMeta();
 
-        assertEquals(0, meta.rowVersionFreeListRootPageId());
+        assertEquals(0, meta.freeListRootPageId());
 
-        assertDoesNotThrow(() -> meta.rowVersionFreeListRootPageId(null, 100));
+        assertDoesNotThrow(() -> meta.freeListRootPageId(null, 100));
 
-        assertEquals(100, meta.rowVersionFreeListRootPageId());
+        assertEquals(100, meta.freeListRootPageId());
 
-        assertDoesNotThrow(() -> meta.rowVersionFreeListRootPageId(UUID.randomUUID(), 500));
+        assertDoesNotThrow(() -> meta.freeListRootPageId(UUID.randomUUID(), 500));
 
-        assertEquals(500, meta.rowVersionFreeListRootPageId());
+        assertEquals(500, meta.freeListRootPageId());
     }
 
     @Test
     void testSnapshot() {
         UUID checkpointId = null;
 
-        PartitionMeta meta = new PartitionMeta(checkpointId, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
+        PartitionMeta meta = new PartitionMeta(checkpointId, 0, 0, 0, 0, 0, 0, 0, 0, 0);
 
         checkSnapshot(meta.metaSnapshot(checkpointId), 0, 0, 0, 0, 0, 0);
         checkSnapshot(meta.metaSnapshot(checkpointId = UUID.randomUUID()), 0, 0, 0, 0, 0, 0);
@@ -138,7 +138,7 @@
         meta.lastApplied(checkpointId, 50, 5);
         meta.lastReplicationProtocolGroupConfigFirstPageId(checkpointId, 12);
         meta.versionChainTreeRootPageId(checkpointId, 300);
-        meta.rowVersionFreeListRootPageId(checkpointId, 900);
+        meta.freeListRootPageId(checkpointId, 900);
         meta.incrementPageCount(checkpointId);
 
         checkSnapshot(meta.metaSnapshot(checkpointId), 0, 0, 0, 0, 0, 0);
@@ -151,7 +151,7 @@
         meta.versionChainTreeRootPageId(checkpointId = UUID.randomUUID(), 303);
         checkSnapshot(meta.metaSnapshot(checkpointId), 51, 6, 34, 300, 900, 1);
 
-        meta.rowVersionFreeListRootPageId(checkpointId = UUID.randomUUID(), 909);
+        meta.freeListRootPageId(checkpointId = UUID.randomUUID(), 909);
         checkSnapshot(meta.metaSnapshot(checkpointId), 51, 6, 34, 303, 900, 1);
 
         meta.incrementPageCount(checkpointId = UUID.randomUUID());
@@ -175,14 +175,14 @@
             long expLastAppliedTerm,
             long expLastGroupConfigFirstPageId,
             long expVersionChainTreeRootPageId,
-            long expRowVersionFreeListRootPageId,
+            long expFreeListRootPageId,
             int expPageCount
     ) {
         assertThat(snapshot.lastAppliedIndex(), equalTo(expLastAppliedIndex));
         assertThat(snapshot.lastAppliedTerm(), equalTo(expLastAppliedTerm));
         assertThat(snapshot.lastReplicationProtocolGroupConfigFirstPageId(), equalTo(expLastGroupConfigFirstPageId));
         assertThat(snapshot.versionChainTreeRootPageId(), equalTo(expVersionChainTreeRootPageId));
-        assertThat(snapshot.rowVersionFreeListRootPageId(), equalTo(expRowVersionFreeListRootPageId));
+        assertThat(snapshot.freeListRootPageId(), equalTo(expFreeListRootPageId));
         assertThat(snapshot.pageCount(), equalTo(expPageCount));
     }
 }
diff --git a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java
index d8c32e1..c685df5 100644
--- a/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java
+++ b/modules/page-memory/src/test/java/org/apache/ignite/internal/pagememory/persistence/checkpoint/CheckpointerTest.java
@@ -359,7 +359,7 @@
 
         partitionMetaManager.addMeta(
                 new GroupPartitionId(0, 0),
-                new PartitionMeta(null, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0)
+                new PartitionMeta(null, 0, 0, 0, 0, 0, 0, 0, 3, 0)
         );
 
         FilePageStore filePageStore = mock(FilePageStore.class);
diff --git a/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/AbstractMvTableStorageTest.java b/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/AbstractMvTableStorageTest.java
index c06efb3..7b66179 100644
--- a/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/AbstractMvTableStorageTest.java
+++ b/modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/AbstractMvTableStorageTest.java
@@ -87,6 +87,7 @@
 import org.apache.ignite.internal.storage.index.StorageIndexDescriptor;
 import org.apache.ignite.internal.storage.index.StorageIndexDescriptorSupplier;
 import org.apache.ignite.internal.storage.index.StorageSortedIndexDescriptor;
+import org.apache.ignite.internal.storage.index.impl.BinaryTupleRowSerializer;
 import org.apache.ignite.internal.type.NativeTypes;
 import org.apache.ignite.internal.util.Cursor;
 import org.jetbrains.annotations.Nullable;
@@ -504,6 +505,68 @@
         assertThat(getAll(storage2.get(tuple)), contains(rowId2));
     }
 
+    @Test
+    public void testPutIndexAndVersionRowToMemory() {
+        MvPartitionStorage partitionStorage = getOrCreateMvPartition(PARTITION_ID);
+        HashIndexStorage indexStorage = tableStorage.getOrCreateHashIndex(PARTITION_ID, hashIdx);
+
+        // Should be large enough to exceed inline size.
+        byte[] bytes = new byte[100];
+        new Random().nextBytes(bytes);
+        String str = new String(bytes);
+
+        BinaryRow binaryRow = binaryRow(new TestKey(10, "foo"), new TestValue(20, str));
+
+        var serializer = new BinaryTupleRowSerializer(indexStorage.indexDescriptor());
+
+        IndexRow indexRow = serializer.serializeRow(new Object[]{str}, new RowId(PARTITION_ID));
+        partitionStorage.runConsistently(locker -> {
+            indexStorage.put(indexRow);
+
+            return null;
+        });
+
+        partitionStorage.runConsistently(locker -> {
+            RowId rowId = new RowId(PARTITION_ID);
+            locker.tryLock(rowId);
+
+            partitionStorage.addWrite(rowId, binaryRow, UUID.randomUUID(), COMMIT_TABLE_ID, PARTITION_ID);
+
+            return null;
+        });
+    }
+
+    @Test
+    public void testPutIndexAndVersionRowToMemoryFragmented() {
+        MvPartitionStorage partitionStorage = getOrCreateMvPartition(PARTITION_ID);
+        HashIndexStorage indexStorage = tableStorage.getOrCreateHashIndex(PARTITION_ID, hashIdx);
+
+        // Should be large enough to exceed memory page size.
+        byte[] bytes = new byte[16385];
+        new Random().nextBytes(bytes);
+        String str = new String(bytes);
+
+        BinaryRow binaryRow = binaryRow(new TestKey(10, "foo"), new TestValue(20, str));
+
+        var serializer = new BinaryTupleRowSerializer(indexStorage.indexDescriptor());
+
+        IndexRow indexRow = serializer.serializeRow(new Object[]{str}, new RowId(PARTITION_ID));
+        partitionStorage.runConsistently(locker -> {
+            indexStorage.put(indexRow);
+
+            return null;
+        });
+
+        partitionStorage.runConsistently(locker -> {
+            RowId rowId = new RowId(PARTITION_ID);
+            locker.tryLock(rowId);
+
+            partitionStorage.addWrite(rowId, binaryRow, UUID.randomUUID(), COMMIT_TABLE_ID, PARTITION_ID);
+
+            return null;
+        });
+    }
+
     private static void checkStorageDestroyed(IndexStorage storage) {
         assertThrows(StorageDestroyedException.class, () -> storage.get(mock(BinaryTuple.class)));
 
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java
index a82d9f2..5795509 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/PersistentPageMemoryTableStorage.java
@@ -30,7 +30,9 @@
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.lang.IgniteStringFormatter;
 import org.apache.ignite.internal.pagememory.PageMemory;
+import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
 import org.apache.ignite.internal.pagememory.evict.PageEvictionTrackerNoOp;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
 import org.apache.ignite.internal.pagememory.persistence.GroupPartitionId;
 import org.apache.ignite.internal.pagememory.persistence.PartitionMeta;
@@ -43,11 +45,9 @@
 import org.apache.ignite.internal.storage.StorageException;
 import org.apache.ignite.internal.storage.engine.StorageTableDescriptor;
 import org.apache.ignite.internal.storage.index.StorageIndexDescriptorSupplier;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.storage.pagememory.mv.AbstractPageMemoryMvPartitionStorage;
 import org.apache.ignite.internal.storage.pagememory.mv.PersistentPageMemoryMvPartitionStorage;
-import org.apache.ignite.internal.storage.pagememory.mv.RowVersionFreeList;
 import org.apache.ignite.internal.storage.pagememory.mv.VersionChainTree;
 import org.apache.ignite.internal.storage.pagememory.mv.gc.GcQueue;
 import org.jetbrains.annotations.Nullable;
@@ -56,6 +56,9 @@
  * Implementation of {@link AbstractPageMemoryTableStorage} for persistent case.
  */
 public class PersistentPageMemoryTableStorage extends AbstractPageMemoryTableStorage {
+    /** String to format free list's name. */
+    private static final String FREE_LIST_NAME = "PersistentFreeList_%d_%d";
+
     /** Storage engine instance. */
     private final PersistentPageMemoryStorageEngine engine;
 
@@ -63,6 +66,7 @@
     private final PersistentPageMemoryDataRegion dataRegion;
 
     private final ExecutorService destructionExecutor;
+    private final PageEvictionTracker pageEvictionTracker = PageEvictionTrackerNoOp.INSTANCE;
 
     /**
      * Constructor.
@@ -117,22 +121,20 @@
         return inCheckpointLock(() -> {
             PersistentPageMemory pageMemory = dataRegion.pageMemory();
 
-            RowVersionFreeList rowVersionFreeList = createRowVersionFreeList(partitionId, pageMemory, meta);
+            FreeListImpl freeList = createFreeList(partitionId, pageMemory, meta);
 
-            IndexColumnsFreeList indexColumnsFreeList = createIndexColumnsFreeList(partitionId, rowVersionFreeList, pageMemory, meta);
+            VersionChainTree versionChainTree = createVersionChainTree(partitionId, freeList, pageMemory, meta);
 
-            VersionChainTree versionChainTree = createVersionChainTree(partitionId, rowVersionFreeList, pageMemory, meta);
+            IndexMetaTree indexMetaTree = createIndexMetaTree(partitionId, freeList, pageMemory, meta);
 
-            IndexMetaTree indexMetaTree = createIndexMetaTree(partitionId, rowVersionFreeList, pageMemory, meta);
-
-            GcQueue gcQueue = createGcQueue(partitionId, rowVersionFreeList, pageMemory, meta);
+            GcQueue gcQueue = createGcQueue(partitionId, freeList, pageMemory, meta);
 
             return new PersistentPageMemoryMvPartitionStorage(
                     this,
                     partitionId,
                     meta,
-                    rowVersionFreeList,
-                    indexColumnsFreeList,
+                    freeList,
+                    pageEvictionTracker,
                     versionChainTree,
                     indexMetaTree,
                     gcQueue,
@@ -151,14 +153,14 @@
     }
 
     /**
-     * Returns new {@link RowVersionFreeList} instance for partition.
+     * Returns new {@link FreeListImpl} instance for partition.
      *
      * @param partId Partition ID.
      * @param pageMemory Persistent page memory instance.
      * @param meta Partition metadata.
      * @throws StorageException If failed.
      */
-    private RowVersionFreeList createRowVersionFreeList(
+    private FreeListImpl createFreeList(
             int partId,
             PersistentPageMemory pageMemory,
             PartitionMeta meta
@@ -166,71 +168,29 @@
         try {
             boolean initNew = false;
 
-            if (meta.rowVersionFreeListRootPageId() == 0) {
+            if (meta.freeListRootPageId() == 0) {
                 long rootPageId = pageMemory.allocatePage(getTableId(), partId, FLAG_AUX);
 
-                meta.rowVersionFreeListRootPageId(lastCheckpointId(), rootPageId);
+                meta.freeListRootPageId(lastCheckpointId(), rootPageId);
 
                 initNew = true;
             }
 
-            return new RowVersionFreeList(
+            return new FreeListImpl(
                     getTableId(),
                     partId,
+                    String.format(FREE_LIST_NAME, getTableId(), partId),
                     dataRegion.pageMemory(),
                     null,
                     PageLockListenerNoOp.INSTANCE,
-                    meta.rowVersionFreeListRootPageId(),
+                    meta.freeListRootPageId(),
                     initNew,
                     dataRegion.pageListCacheLimit(),
-                    PageEvictionTrackerNoOp.INSTANCE,
+                    pageEvictionTracker,
                     IoStatisticsHolderNoOp.INSTANCE
             );
         } catch (IgniteInternalCheckedException e) {
-            throw new StorageException("Error creating RowVersionFreeList: [tableId={}, partitionId={}]", e, getTableId(), partId);
-        }
-    }
-
-    /**
-     * Returns new {@link IndexColumnsFreeList} instance for partition.
-     *
-     * @param partitionId Partition ID.
-     * @param reuseList Reuse list.
-     * @param pageMemory Persistent page memory instance.
-     * @param meta Partition metadata.
-     * @throws StorageException If failed.
-     */
-    private IndexColumnsFreeList createIndexColumnsFreeList(
-            int partitionId,
-            ReuseList reuseList,
-            PersistentPageMemory pageMemory,
-            PartitionMeta meta
-    ) {
-        try {
-            boolean initNew = false;
-
-            if (meta.indexColumnsFreeListRootPageId() == 0L) {
-                long rootPageId = pageMemory.allocatePage(getTableId(), partitionId, FLAG_AUX);
-
-                meta.indexColumnsFreeListRootPageId(lastCheckpointId(), rootPageId);
-
-                initNew = true;
-            }
-
-            return new IndexColumnsFreeList(
-                    getTableId(),
-                    partitionId,
-                    pageMemory,
-                    reuseList,
-                    PageLockListenerNoOp.INSTANCE,
-                    meta.indexColumnsFreeListRootPageId(),
-                    initNew,
-                    new AtomicLong(),
-                    PageEvictionTrackerNoOp.INSTANCE,
-                    IoStatisticsHolderNoOp.INSTANCE
-            );
-        } catch (IgniteInternalCheckedException e) {
-            throw new StorageException("Error creating IndexColumnsFreeList: [tableId={}, partitionId={}]", e, getTableId(), partitionId);
+            throw new StorageException("Error creating free list: [tableId={}, partitionId={}]", e, getTableId(), partId);
         }
     }
 
@@ -384,20 +344,17 @@
             PartitionMeta meta = getOrCreatePartitionMetaOnCreatePartition(groupPartitionId);
 
             inCheckpointLock(() -> {
-                RowVersionFreeList rowVersionFreeList = createRowVersionFreeList(partitionId, pageMemory, meta);
+                FreeListImpl freeList = createFreeList(partitionId, pageMemory, meta);
 
-                IndexColumnsFreeList indexColumnsFreeList = createIndexColumnsFreeList(partitionId, rowVersionFreeList, pageMemory, meta);
+                VersionChainTree versionChainTree = createVersionChainTree(partitionId, freeList, pageMemory, meta);
 
-                VersionChainTree versionChainTree = createVersionChainTree(partitionId, rowVersionFreeList, pageMemory, meta);
+                IndexMetaTree indexMetaTree = createIndexMetaTree(partitionId, freeList, pageMemory, meta);
 
-                IndexMetaTree indexMetaTree = createIndexMetaTree(partitionId, rowVersionFreeList, pageMemory, meta);
-
-                GcQueue gcQueue = createGcQueue(partitionId, rowVersionFreeList, pageMemory, meta);
+                GcQueue gcQueue = createGcQueue(partitionId, freeList, pageMemory, meta);
 
                 ((PersistentPageMemoryMvPartitionStorage) mvPartitionStorage).updateDataStructures(
                         meta,
-                        rowVersionFreeList,
-                        indexColumnsFreeList,
+                        freeList,
                         versionChainTree,
                         indexMetaTree,
                         gcQueue
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java
index 8b27224..04bda0d 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryDataRegion.java
@@ -25,14 +25,13 @@
 import org.apache.ignite.internal.pagememory.PageMemory;
 import org.apache.ignite.internal.pagememory.configuration.schema.VolatilePageMemoryProfileConfiguration;
 import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.inmemory.VolatilePageMemory;
 import org.apache.ignite.internal.pagememory.io.PageIoRegistry;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
 import org.apache.ignite.internal.pagememory.reuse.ReuseList;
 import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.storage.StorageException;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
-import org.apache.ignite.internal.storage.pagememory.mv.RowVersionFreeList;
 
 /**
  * Implementation of {@link DataRegion} for in-memory case.
@@ -42,6 +41,8 @@
 
     private static final int FREE_LIST_PARTITION_ID = 0;
 
+    private static final String FREE_LIST_NAME = String.format("VolatileFreeList_%d_%d", FREE_LIST_GROUP_ID, FREE_LIST_PARTITION_ID);
+
     private final VolatilePageMemoryProfileConfiguration cfg;
 
     private final PageIoRegistry ioRegistry;
@@ -52,9 +53,7 @@
 
     private volatile VolatilePageMemory pageMemory;
 
-    private volatile RowVersionFreeList rowVersionFreeList;
-
-    private volatile IndexColumnsFreeList indexColumnsFreeList;
+    private volatile FreeListImpl freeList;
 
     /**
      * Constructor.
@@ -85,24 +84,23 @@
         pageMemory.start();
 
         try {
-            rowVersionFreeList = createRowVersionFreeList(pageMemory);
-
-            indexColumnsFreeList = createIndexColumnsFreeList(pageMemory, rowVersionFreeList);
+            this.freeList = createFreeList(pageMemory);
         } catch (IgniteInternalCheckedException e) {
-            throw new StorageException("Error creating a RowVersionFreeList", e);
+            throw new StorageException("Error creating free list", e);
         }
 
         this.pageMemory = pageMemory;
     }
 
-    private RowVersionFreeList createRowVersionFreeList(
+    private FreeListImpl createFreeList(
             PageMemory pageMemory
     ) throws IgniteInternalCheckedException {
         long metaPageId = pageMemory.allocatePage(FREE_LIST_GROUP_ID, FREE_LIST_PARTITION_ID, FLAG_AUX);
 
-        return new RowVersionFreeList(
+        return new FreeListImpl(
                 FREE_LIST_GROUP_ID,
                 FREE_LIST_PARTITION_ID,
+                FREE_LIST_NAME,
                 pageMemory,
                 null,
                 PageLockListenerNoOp.INSTANCE,
@@ -115,32 +113,12 @@
         );
     }
 
-    private IndexColumnsFreeList createIndexColumnsFreeList(VolatilePageMemory pageMemory, ReuseList reuseList)
-            throws IgniteInternalCheckedException {
-        long metaPageId = pageMemory.allocatePage(FREE_LIST_GROUP_ID, FREE_LIST_PARTITION_ID, FLAG_AUX);
-
-        return new IndexColumnsFreeList(
-                FREE_LIST_GROUP_ID,
-                FREE_LIST_PARTITION_ID,
-                pageMemory,
-                reuseList,
-                PageLockListenerNoOp.INSTANCE,
-                metaPageId,
-                true,
-                // Because in memory.
-                null,
-                pageEvictionTracker,
-                IoStatisticsHolderNoOp.INSTANCE
-        );
-    }
-
     /**
      * Starts the in-memory data region.
      */
     public void stop() throws Exception {
         closeAllManually(
-                rowVersionFreeList,
-                indexColumnsFreeList,
+                freeList,
                 pageMemory != null ? () -> pageMemory.stop(true) : null
         );
     }
@@ -154,7 +132,7 @@
     }
 
     public ReuseList reuseList() {
-        return rowVersionFreeList();
+        return freeList;
     }
 
     /**
@@ -162,14 +140,10 @@
      *
      * @throws StorageException If the data region did not start.
      */
-    public RowVersionFreeList rowVersionFreeList() {
+    public FreeListImpl freeList() {
         checkDataRegionStarted();
 
-        return rowVersionFreeList;
-    }
-
-    public IndexColumnsFreeList indexColumnsFreeList() {
-        return indexColumnsFreeList;
+        return freeList;
     }
 
     /**
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryStorageEngine.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryStorageEngine.java
index 16c1ac7..92f7ef5 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryStorageEngine.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryStorageEngine.java
@@ -150,7 +150,13 @@
 
         assert dataRegion != null : "tableId=" + tableDescriptor.getId() + ", dataRegion=" + tableDescriptor.getStorageProfile();
 
-        return new VolatilePageMemoryTableStorage(tableDescriptor, indexDescriptorSupplier, dataRegion, destructionExecutor);
+        return new VolatilePageMemoryTableStorage(
+                tableDescriptor,
+                indexDescriptorSupplier,
+                dataRegion,
+                destructionExecutor,
+                pageEvictionTracker
+        );
     }
 
     @Override
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java
index 4da30fa..768e673 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryTableStorage.java
@@ -24,6 +24,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicLong;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
 import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.storage.StorageException;
 import org.apache.ignite.internal.storage.engine.StorageTableDescriptor;
@@ -41,6 +42,7 @@
     private final VolatilePageMemoryDataRegion dataRegion;
 
     private final ExecutorService destructionExecutor;
+    private final PageEvictionTracker pageEvictionTracker;
 
     /**
      * Constructor.
@@ -49,17 +51,20 @@
      * @param indexDescriptorSupplier Index descriptor supplier.
      * @param dataRegion Data region for the table.
      * @param destructionExecutor Executor used to destruct partitions.
+     * @param pageEvictionTracker Page eviction tracker.
      */
     VolatilePageMemoryTableStorage(
             StorageTableDescriptor tableDescriptor,
             StorageIndexDescriptorSupplier indexDescriptorSupplier,
             VolatilePageMemoryDataRegion dataRegion,
-            ExecutorService destructionExecutor
+            ExecutorService destructionExecutor,
+            PageEvictionTracker pageEvictionTracker
     ) {
         super(tableDescriptor, indexDescriptorSupplier);
 
         this.dataRegion = dataRegion;
         this.destructionExecutor = destructionExecutor;
+        this.pageEvictionTracker = pageEvictionTracker;
     }
 
     @Override
@@ -78,6 +83,7 @@
         return new VolatilePageMemoryMvPartitionStorage(
                 this,
                 partitionId,
+                pageEvictionTracker,
                 versionChainTree,
                 indexMetaTree,
                 gcQueue,
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/AbstractPageMemoryIndexStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/AbstractPageMemoryIndexStorage.java
index 76b55e8..284052b 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/AbstractPageMemoryIndexStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/AbstractPageMemoryIndexStorage.java
@@ -32,6 +32,7 @@
 import org.apache.ignite.internal.lang.IgniteStringFormatter;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.pagememory.util.GradualTask;
 import org.apache.ignite.internal.pagememory.util.GradualTaskExecutor;
@@ -43,7 +44,6 @@
 import org.apache.ignite.internal.storage.pagememory.PersistentPageMemoryStorageEngine;
 import org.apache.ignite.internal.storage.pagememory.VolatilePageMemoryStorageEngine;
 import org.apache.ignite.internal.storage.pagememory.index.common.IndexRowKey;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMeta;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaKey;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
@@ -75,8 +75,8 @@
     /** Index meta tree instance. */
     private volatile IndexMetaTree indexMetaTree;
 
-    /** Free list to store index columns. */
-    protected volatile IndexColumnsFreeList freeList;
+    /** Free list. */
+    protected volatile FreeListImpl freeList;
 
     /** Index tree instance. */
     protected volatile TreeT indexTree;
@@ -96,7 +96,7 @@
             IndexMeta indexMeta,
             int partitionId,
             TreeT indexTree,
-            IndexColumnsFreeList freeList,
+            FreeListImpl freeList,
             IndexMetaTree indexMetaTree,
             boolean isVolatile
     ) {
@@ -282,11 +282,11 @@
     /**
      * Updates the internal data structures of the storage on rebalance or cleanup.
      *
-     * @param freeList Free list to store index columns.
+     * @param freeList Free list.
      * @param indexTree Hash index tree instance.
      * @throws StorageException If failed.
      */
-    public void updateDataStructures(IndexMetaTree indexMetaTree, IndexColumnsFreeList freeList, TreeT indexTree) {
+    public void updateDataStructures(IndexMetaTree indexMetaTree, FreeListImpl freeList, TreeT indexTree) {
         throwExceptionIfStorageNotInCleanupOrRebalancedState(state.get(), this::createStorageInfo);
 
         this.indexMetaTree = indexMetaTree;
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageIoModule.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageIoModule.java
index 9840394..0ddb772 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageIoModule.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageIoModule.java
@@ -24,7 +24,6 @@
 import org.apache.ignite.internal.pagememory.PageMemory;
 import org.apache.ignite.internal.pagememory.io.IoVersions;
 import org.apache.ignite.internal.pagememory.io.PageIoModule;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.io.IndexColumnsDataIo;
 import org.apache.ignite.internal.storage.pagememory.index.hash.io.HashIndexTreeInnerIo;
 import org.apache.ignite.internal.storage.pagememory.index.hash.io.HashIndexTreeLeafIo;
 import org.apache.ignite.internal.storage.pagememory.index.hash.io.HashIndexTreeMetaIo;
@@ -45,8 +44,6 @@
     public Collection<IoVersions<?>> ioVersions() {
         List<IoVersions<?>> ioVersions = new ArrayList<>();
 
-        ioVersions.add(IndexColumnsDataIo.VERSIONS);
-
         // Meta tree IO.
         ioVersions.add(IndexMetaTreeMetaIo.VERSIONS);
         ioVersions.add(IndexMetaInnerIo.VERSIONS);
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageTypes.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageTypes.java
index 8eab57d..2d98192 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageTypes.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/IndexPageTypes.java
@@ -21,20 +21,17 @@
  * Collection of all page types that relate to indexes.
  */
 public interface IndexPageTypes {
-    /** Index columns IO type. */
-    short T_INDEX_COLUMNS_DATA_IO = 100;
-
     /** Index meta tree meta IO type. */
-    short T_INDEX_META_TREE_META_IO = 101;
+    short T_INDEX_META_TREE_META_IO = 100;
 
     /** Index meta tree inner IO type. */
-    short T_INDEX_META_INNER_IO = 102;
+    short T_INDEX_META_INNER_IO = 101;
 
     /** Index meta tree leaf IO type. */
-    short T_INDEX_META_LEAF_IO = 103;
+    short T_INDEX_META_LEAF_IO = 102;
 
     /** Hash index tree meta IO type. */
-    short T_HASH_INDEX_META_IO = 104;
+    short T_HASH_INDEX_META_IO = 103;
 
     /** Starting hash index tree inner IO type. No more than the {@link InlineUtils#MAX_BINARY_TUPLE_INLINE_SIZE}. */
     short T_HASH_INDEX_INNER_IO_START = 10_000;
@@ -43,7 +40,7 @@
     short T_HASH_INDEX_LEAF_IO_START = 15_000;
 
     /** Sorted index tree meta IO type. */
-    short T_SORTED_INDEX_META_IO = 105;
+    short T_SORTED_INDEX_META_IO = 104;
 
     /** Starting sorted index tree inner IO type. No more than the {@link InlineUtils#MAX_BINARY_TUPLE_INLINE_SIZE}. */
     short T_SORTED_INDEX_INNER_IO_START = 20_000;
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumns.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumns.java
index 9d9e2fa..60961cc 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumns.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumns.java
@@ -22,17 +22,17 @@
 import java.nio.ByteBuffer;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.pagememory.Storable;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
-import org.apache.ignite.internal.pagememory.io.IoVersions;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.io.IndexColumnsDataIo;
+import org.apache.ignite.internal.pagememory.util.PageUtils;
 import org.jetbrains.annotations.Nullable;
 
 /**
  * Index columns to store in free list.
  */
 public class IndexColumns implements Storable {
+    public static final byte DATA_TYPE = 1;
+
     /** Size offset. */
-    public static final int SIZE_OFFSET = 0;
+    public static final int SIZE_OFFSET = DATA_TYPE_OFFSET + DATA_TYPE_SIZE_BYTES;
 
     /** Value offset. Value goes right after the size. */
     public static final int VALUE_OFFSET = SIZE_OFFSET + Integer.BYTES;
@@ -113,7 +113,38 @@
     }
 
     @Override
-    public IoVersions<? extends AbstractDataPageIo<?>> ioVersions() {
-        return IndexColumnsDataIo.VERSIONS;
+    public void writeRowData(long pageAddr, int dataOff, int payloadSize, boolean newRow) {
+        PageUtils.putShort(pageAddr, dataOff, (short) payloadSize);
+
+        dataOff += Short.BYTES;
+
+        PageUtils.putByte(pageAddr, dataOff + DATA_TYPE_OFFSET, DATA_TYPE);
+
+        PageUtils.putInt(pageAddr, dataOff + SIZE_OFFSET, valueSize());
+
+        PageUtils.putByteBuffer(pageAddr, dataOff + VALUE_OFFSET, valueBuffer());
+    }
+
+    @Override
+    public void writeFragmentData(
+            ByteBuffer pageBuf,
+            int rowOff,
+            int payloadSize
+    ) {
+        if (rowOff == 0) {
+            // First fragment.
+            assert headerSize() <= payloadSize;
+
+            pageBuf.put(DATA_TYPE);
+
+            pageBuf.putInt(valueSize());
+
+            Storable.putValueBufferIntoPage(pageBuf, valueBuffer, 0, payloadSize - VALUE_OFFSET);
+        } else {
+            // Not a first fragment.
+            assert rowOff >= headerSize();
+
+            Storable.putValueBufferIntoPage(pageBuf, valueBuffer, rowOff - VALUE_OFFSET, payloadSize);
+        }
     }
 }
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumnsFreeList.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumnsFreeList.java
deleted file mode 100644
index c1acdc6..0000000
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/IndexColumnsFreeList.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.storage.pagememory.index.freelist;
-
-import java.util.concurrent.atomic.AtomicLong;
-import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
-import org.apache.ignite.internal.logger.IgniteLogger;
-import org.apache.ignite.internal.logger.Loggers;
-import org.apache.ignite.internal.pagememory.PageMemory;
-import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
-import org.apache.ignite.internal.pagememory.freelist.AbstractFreeList;
-import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
-import org.apache.ignite.internal.pagememory.reuse.ReuseList;
-import org.apache.ignite.internal.pagememory.util.PageLockListener;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Free list implementation to store {@link IndexColumns} values.
- */
-public class IndexColumnsFreeList extends AbstractFreeList<IndexColumns>  {
-    private static final IgniteLogger LOG = Loggers.forClass(IndexColumnsFreeList.class);
-
-    private final IoStatisticsHolder statHolder;
-
-    /**
-     * Constructor.
-     *
-     * @param grpId Group ID.
-     * @param partId Partition ID.
-     * @param pageMem Page memory.
-     * @param reuseList Reuse list or {@code null} if this free list will be a reuse list for itself.
-     * @param lockLsnr Page lock listener.
-     * @param metaPageId Metadata page ID.
-     * @param initNew {@code True} if new metadata should be initialized.
-     * @param pageListCacheLimit Page list cache limit.
-     * @param evictionTracker Page eviction tracker.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public IndexColumnsFreeList(
-            int grpId,
-            int partId,
-            PageMemory pageMem,
-            @Nullable ReuseList reuseList,
-            PageLockListener lockLsnr,
-            long metaPageId,
-            boolean initNew,
-            @Nullable AtomicLong pageListCacheLimit,
-            PageEvictionTracker evictionTracker,
-            IoStatisticsHolder statHolder
-    ) throws IgniteInternalCheckedException {
-        super(
-                grpId,
-                partId,
-                "IndexColumnsFreeList_" + grpId,
-                pageMem,
-                reuseList,
-                lockLsnr,
-                LOG,
-                metaPageId,
-                initNew,
-                pageListCacheLimit,
-                evictionTracker
-        );
-
-        this.statHolder = statHolder;
-    }
-
-    /**
-     * Shortcut method for {@link #saveMetadata(IoStatisticsHolder)} with statistics holder.
-     *
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public void saveMetadata() throws IgniteInternalCheckedException {
-        saveMetadata(statHolder);
-    }
-
-    /**
-     * Shortcut method for {@link #insertDataRow(IndexColumns, IoStatisticsHolder)} with statistics holder.
-     */
-    public void insertDataRow(IndexColumns row) throws IgniteInternalCheckedException {
-        super.insertDataRow(row, statHolder);
-    }
-
-    /**
-     * Shortcut method for {@link #removeDataRowByLink(long, IoStatisticsHolder)} with statistics holder.
-     */
-    public void removeDataRowByLink(long link) throws IgniteInternalCheckedException {
-        super.removeDataRowByLink(link, statHolder);
-    }
-}
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/ReadIndexColumnsValue.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/ReadIndexColumnsValue.java
index 311997d..bf36f67 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/ReadIndexColumnsValue.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/ReadIndexColumnsValue.java
@@ -33,4 +33,9 @@
     protected int valueOffsetInFirstSlot() {
         return IndexColumns.VALUE_OFFSET;
     }
+
+    @Override
+    protected byte dataType() {
+        return IndexColumns.DATA_TYPE;
+    }
 }
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/io/IndexColumnsDataIo.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/io/IndexColumnsDataIo.java
deleted file mode 100644
index ee4d93b..0000000
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/freelist/io/IndexColumnsDataIo.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.storage.pagememory.index.freelist.io;
-
-import static org.apache.ignite.internal.pagememory.util.PageUtils.putByteBuffer;
-import static org.apache.ignite.internal.pagememory.util.PageUtils.putInt;
-import static org.apache.ignite.internal.pagememory.util.PageUtils.putShort;
-import static org.apache.ignite.internal.storage.pagememory.index.IndexPageTypes.T_INDEX_COLUMNS_DATA_IO;
-
-import java.nio.ByteBuffer;
-import org.apache.ignite.internal.lang.IgniteStringBuilder;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
-import org.apache.ignite.internal.pagememory.io.IoVersions;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-
-/**
- * Data pages IO for {@link IndexColumns}.
- */
-public class IndexColumnsDataIo extends AbstractDataPageIo<IndexColumns> {
-    /** I/O versions. */
-    public static final IoVersions<IndexColumnsDataIo> VERSIONS = new IoVersions<>(new IndexColumnsDataIo(1));
-
-    /**
-     * Constructor.
-     *
-     * @param ver Page format version.
-     */
-    protected IndexColumnsDataIo(int ver) {
-        super(T_INDEX_COLUMNS_DATA_IO, ver);
-    }
-
-    @Override
-    protected void writeRowData(long pageAddr, int dataOff, int payloadSize, IndexColumns row, boolean newRow) {
-        assertPageType(pageAddr);
-
-        putShort(pageAddr, dataOff, (short) payloadSize);
-
-        dataOff += Short.BYTES;
-
-        putInt(pageAddr, dataOff + IndexColumns.SIZE_OFFSET, row.valueSize());
-
-        putByteBuffer(pageAddr, dataOff + IndexColumns.VALUE_OFFSET, row.valueBuffer());
-    }
-
-    @Override
-    protected void writeFragmentData(IndexColumns row, ByteBuffer pageBuf, int rowOff, int payloadSize) {
-        assertPageType(pageBuf);
-
-        if (rowOff == 0) {
-            // First fragment.
-            assert row.headerSize() <= payloadSize;
-
-            pageBuf.putInt(row.valueSize());
-
-            putValueBufferIntoPage(pageBuf, row.valueBuffer(), 0, payloadSize - IndexColumns.VALUE_OFFSET);
-        } else {
-            // Not a first fragment.
-            assert rowOff >= row.headerSize();
-
-            putValueBufferIntoPage(pageBuf, row.valueBuffer(), rowOff - IndexColumns.VALUE_OFFSET, payloadSize);
-        }
-    }
-
-    @Override
-    protected void printPage(long addr, int pageSize, IgniteStringBuilder sb) {
-        sb.app("IndexColumnsDataIo [\n");
-        printPageLayout(addr, pageSize, sb);
-        sb.app("\n]");
-    }
-}
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java
index 54b6cd9..f296261 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/InsertHashIndexRowInvokeClosure.java
@@ -21,14 +21,14 @@
 import static org.apache.ignite.internal.storage.pagememory.index.InlineUtils.canFullyInline;
 
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.OperationType;
 import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Insert closure that inserts corresponding {@link IndexColumns} into a {@link IndexColumnsFreeList} before writing to the {@link
+ * Insert closure that inserts corresponding {@link IndexColumns} into a {@link FreeListImpl} before writing to the {@link
  * HashIndexTree}.
  */
 class InsertHashIndexRowInvokeClosure implements InvokeClosure<HashIndexRow> {
@@ -36,7 +36,7 @@
     private final HashIndexRow hashIndexRow;
 
     /** Free list to insert data into in case of necessity. */
-    private final IndexColumnsFreeList freeList;
+    private final FreeListImpl freeList;
 
     /** Inline size in bytes. */
     private final int inlineSize;
@@ -51,7 +51,7 @@
      * @param freeList Free list to insert data into in case of necessity.
      * @param inlineSize Inline size in bytes.
      */
-    public InsertHashIndexRowInvokeClosure(HashIndexRow hashIndexRow, IndexColumnsFreeList freeList, int inlineSize) {
+    public InsertHashIndexRowInvokeClosure(HashIndexRow hashIndexRow, FreeListImpl freeList, int inlineSize) {
         assert hashIndexRow.indexColumns().link() == NULL_LINK;
 
         this.hashIndexRow = hashIndexRow;
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java
index c3fdc11..a510ff7 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java
@@ -21,6 +21,7 @@
 
 import java.util.Objects;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.util.GradualTask;
 import org.apache.ignite.internal.pagememory.util.PageIdUtils;
 import org.apache.ignite.internal.schema.BinaryTuple;
@@ -31,7 +32,6 @@
 import org.apache.ignite.internal.storage.index.StorageHashIndexDescriptor;
 import org.apache.ignite.internal.storage.pagememory.index.AbstractPageMemoryIndexStorage;
 import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMeta;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.util.Cursor;
@@ -55,14 +55,14 @@
      *
      * @param indexMeta Index meta.
      * @param descriptor Hash index descriptor.
-     * @param freeList Free list to store index columns.
+     * @param freeList Free list.
      * @param indexTree Hash index tree instance.
      * @param indexMetaTree Index meta tree instance.
      */
     public PageMemoryHashIndexStorage(
             IndexMeta indexMeta,
             @Nullable StorageHashIndexDescriptor descriptor,
-            IndexColumnsFreeList freeList,
+            FreeListImpl freeList,
             HashIndexTree indexTree,
             IndexMetaTree indexMetaTree,
             boolean isVolatile
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java
index 35b3c99..4572a26 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/RemoveHashIndexRowInvokeClosure.java
@@ -20,15 +20,15 @@
 import static org.apache.ignite.internal.pagememory.util.PageIdUtils.NULL_LINK;
 
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.OperationType;
 import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Insert closure that removes corresponding {@link IndexColumns} from a {@link IndexColumnsFreeList} after removing it from the {@link
+ * Insert closure that removes corresponding {@link IndexColumns} from a {@link FreeListImpl} after removing it from the {@link
  * HashIndexTree}.
  */
 class RemoveHashIndexRowInvokeClosure implements InvokeClosure<HashIndexRow> {
@@ -36,7 +36,7 @@
     private final HashIndexRow hashIndexRow;
 
     /** Free list to insert data into in case of necessity. */
-    private final IndexColumnsFreeList freeList;
+    private final FreeListImpl freeList;
 
     /** Operation type, either {@link OperationType#REMOVE} or {@link OperationType#NOOP} if row is missing. */
     private OperationType operationType = OperationType.REMOVE;
@@ -47,7 +47,7 @@
      * @param hashIndexRow Hash index row instance for removal.
      * @param freeList Free list to insert data into in case of necessity.
      */
-    public RemoveHashIndexRowInvokeClosure(HashIndexRow hashIndexRow, IndexColumnsFreeList freeList) {
+    public RemoveHashIndexRowInvokeClosure(HashIndexRow hashIndexRow, FreeListImpl freeList) {
         assert hashIndexRow.indexColumns().link() == NULL_LINK;
 
         this.hashIndexRow = hashIndexRow;
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/InsertSortedIndexRowInvokeClosure.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/InsertSortedIndexRowInvokeClosure.java
index 66fec49..9a2ae9b 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/InsertSortedIndexRowInvokeClosure.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/InsertSortedIndexRowInvokeClosure.java
@@ -21,14 +21,14 @@
 import static org.apache.ignite.internal.storage.pagememory.index.InlineUtils.canFullyInline;
 
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.OperationType;
 import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Insert closure that inserts corresponding {@link IndexColumns} into a {@link IndexColumnsFreeList} before writing to the {@link
+ * Insert closure that inserts corresponding {@link IndexColumns} into a {@link FreeListImpl} before writing to the {@link
  * SortedIndexTree}.
  */
 class InsertSortedIndexRowInvokeClosure implements InvokeClosure<SortedIndexRow> {
@@ -36,7 +36,7 @@
     private final SortedIndexRow sortedIndexRow;
 
     /** Free list to insert data into in case of necessity. */
-    private final IndexColumnsFreeList freeList;
+    private final FreeListImpl freeList;
 
     /** Operation type, either {@link OperationType#PUT} or {@link OperationType#NOOP} depending on the tree state. */
     private OperationType operationType = OperationType.PUT;
@@ -51,7 +51,7 @@
      * @param freeList Free list to insert data into in case of necessity.
      * @param inlineSize Inline size in bytes.
      */
-    public InsertSortedIndexRowInvokeClosure(SortedIndexRow sortedIndexRow, IndexColumnsFreeList freeList, int inlineSize) {
+    public InsertSortedIndexRowInvokeClosure(SortedIndexRow sortedIndexRow, FreeListImpl freeList, int inlineSize) {
         assert sortedIndexRow.indexColumns().link() == NULL_LINK;
 
         this.sortedIndexRow = sortedIndexRow;
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/PageMemorySortedIndexStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/PageMemorySortedIndexStorage.java
index c79b505..2c58b40 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/PageMemorySortedIndexStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/PageMemorySortedIndexStorage.java
@@ -23,6 +23,7 @@
 import java.util.Objects;
 import org.apache.ignite.internal.binarytuple.BinaryTupleCommon;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.util.GradualTask;
 import org.apache.ignite.internal.pagememory.util.PageIdUtils;
 import org.apache.ignite.internal.schema.BinaryTuple;
@@ -37,7 +38,6 @@
 import org.apache.ignite.internal.storage.index.StorageSortedIndexDescriptor;
 import org.apache.ignite.internal.storage.pagememory.index.AbstractPageMemoryIndexStorage;
 import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMeta;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.util.Cursor;
@@ -61,14 +61,14 @@
      *
      * @param indexMeta Index meta.
      * @param descriptor Sorted index descriptor.
-     * @param freeList Free list to store index columns.
+     * @param freeList Free list.
      * @param indexTree Sorted index tree instance.
      * @param indexMetaTree Index meta tree instance.
      */
     public PageMemorySortedIndexStorage(
             IndexMeta indexMeta,
             @Nullable StorageSortedIndexDescriptor descriptor,
-            IndexColumnsFreeList freeList,
+            FreeListImpl freeList,
             SortedIndexTree indexTree,
             IndexMetaTree indexMetaTree,
             boolean isVolatile
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/RemoveSortedIndexRowInvokeClosure.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/RemoveSortedIndexRowInvokeClosure.java
index 0453792..4a8bda7 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/RemoveSortedIndexRowInvokeClosure.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/RemoveSortedIndexRowInvokeClosure.java
@@ -20,15 +20,15 @@
 import static org.apache.ignite.internal.pagememory.util.PageIdUtils.NULL_LINK;
 
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.OperationType;
 import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Insert closure that removes corresponding {@link IndexColumns} from a {@link IndexColumnsFreeList} after removing it from the {@link
+ * Insert closure that removes corresponding {@link IndexColumns} from a {@link FreeListImpl} after removing it from the {@link
  * SortedIndexTree}.
  */
 class RemoveSortedIndexRowInvokeClosure implements InvokeClosure<SortedIndexRow> {
@@ -36,7 +36,7 @@
     private final SortedIndexRow sortedIndexRow;
 
     /** Free list to insert data into in case of necessity. */
-    private final IndexColumnsFreeList freeList;
+    private final FreeListImpl freeList;
 
     /** Operation type, either {@link OperationType#REMOVE} or {@link OperationType#NOOP} if row is missing. */
     private OperationType operationType = OperationType.REMOVE;
@@ -47,7 +47,7 @@
      * @param sortedIndexRow Sorted index row instance for removal.
      * @param freeList Free list to insert data into in case of necessity.
      */
-    public RemoveSortedIndexRowInvokeClosure(SortedIndexRow sortedIndexRow, IndexColumnsFreeList freeList) {
+    public RemoveSortedIndexRowInvokeClosure(SortedIndexRow sortedIndexRow, FreeListImpl freeList) {
         assert sortedIndexRow.indexColumns().link() == 0L;
 
         this.sortedIndexRow = sortedIndexRow;
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java
index edd485d..0b3aa11 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/AbstractPageMemoryMvPartitionStorage.java
@@ -37,6 +37,8 @@
 import org.apache.ignite.internal.lang.IgniteStringFormatter;
 import org.apache.ignite.internal.pagememory.PageMemory;
 import org.apache.ignite.internal.pagememory.datapage.DataPageReader;
+import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
 import org.apache.ignite.internal.pagememory.tree.BplusTree.TreeRowMapClosure;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure;
@@ -55,11 +57,12 @@
 import org.apache.ignite.internal.storage.index.StorageHashIndexDescriptor;
 import org.apache.ignite.internal.storage.index.StorageSortedIndexDescriptor;
 import org.apache.ignite.internal.storage.pagememory.AbstractPageMemoryTableStorage;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.hash.PageMemoryHashIndexStorage;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.storage.pagememory.index.sorted.PageMemorySortedIndexStorage;
+import org.apache.ignite.internal.storage.pagememory.mv.CommitWriteInvokeClosure.UpdateTimestampHandler;
 import org.apache.ignite.internal.storage.pagememory.mv.FindRowVersion.RowVersionFilter;
+import org.apache.ignite.internal.storage.pagememory.mv.RemoveWriteOnGcInvokeClosure.UpdateNextLinkHandler;
 import org.apache.ignite.internal.storage.pagememory.mv.gc.GcQueue;
 import org.apache.ignite.internal.storage.pagememory.mv.gc.GcRowVersion;
 import org.apache.ignite.internal.storage.util.LocalLocker;
@@ -112,15 +115,21 @@
     /** Busy lock. */
     private final IgniteSpinBusyLock busyLock = new IgniteSpinBusyLock();
 
+    private final UpdateNextLinkHandler updateNextLinkHandler;
+
+    private final UpdateTimestampHandler updateTimestampHandler;
+
     /**
      * Constructor.
      *
      * @param partitionId Partition ID.
+     * @param pageEvictionTracker Page eviction tracker.
      * @param tableStorage Table storage instance.
      */
     AbstractPageMemoryMvPartitionStorage(
             int partitionId,
             AbstractPageMemoryTableStorage tableStorage,
+            PageEvictionTracker pageEvictionTracker,
             RenewablePartitionStorageState renewableState,
             ExecutorService destructionExecutor
     ) {
@@ -133,6 +142,8 @@
         PageMemory pageMemory = tableStorage.dataRegion().pageMemory();
 
         rowVersionDataPageReader = new DataPageReader(pageMemory, tableStorage.getTableId(), IoStatisticsHolderNoOp.INSTANCE);
+        updateNextLinkHandler = new UpdateNextLinkHandler(pageEvictionTracker);
+        updateTimestampHandler = new UpdateTimestampHandler(pageEvictionTracker);
     }
 
     protected abstract GradualTaskExecutor createGradualTaskExecutor(ExecutorService threadPool);
@@ -187,8 +198,7 @@
 
     void updateRenewableState(
             VersionChainTree versionChainTree,
-            RowVersionFreeList rowVersionFreeList,
-            IndexColumnsFreeList indexFreeList,
+            FreeListImpl freeList,
             IndexMetaTree indexMetaTree,
             GcQueue gcQueue
     ) {
@@ -196,8 +206,7 @@
                 tableStorage,
                 partitionId,
                 versionChainTree,
-                rowVersionFreeList,
-                indexFreeList,
+                freeList,
                 indexMetaTree,
                 gcQueue
         );
@@ -399,7 +408,7 @@
 
     void insertRowVersion(RowVersion rowVersion) {
         try {
-            renewableState.rowVersionFreeList().insertDataRow(rowVersion);
+            renewableState.freeList().insertDataRow(rowVersion);
         } catch (IgniteInternalCheckedException e) {
             throw new StorageException("Cannot store a row version: [row={}, {}]", e, rowVersion, createStorageInfo());
         }
@@ -470,7 +479,12 @@
             assert rowIsLocked(rowId);
 
             try {
-                CommitWriteInvokeClosure commitWrite = new CommitWriteInvokeClosure(rowId, timestamp, this);
+                CommitWriteInvokeClosure commitWrite = new CommitWriteInvokeClosure(
+                        rowId,
+                        timestamp,
+                        updateTimestampHandler,
+                        this
+                );
 
                 renewableState.versionChainTree().invoke(new VersionChainKey(rowId), null, commitWrite);
 
@@ -487,7 +501,7 @@
 
     void removeRowVersion(RowVersion rowVersion) {
         try {
-            renewableState.rowVersionFreeList().removeDataRowByLink(rowVersion.link());
+            renewableState.freeList().removeDataRowByLink(rowVersion.link());
         } catch (IgniteInternalCheckedException e) {
             throw new StorageException("Cannot remove row version: [row={}, {}]", e, rowVersion, createStorageInfo());
         }
@@ -851,7 +865,13 @@
     }
 
     private RowVersion removeWriteOnGc(RowId rowId, HybridTimestamp rowTimestamp, long rowLink) {
-        RemoveWriteOnGcInvokeClosure removeWriteOnGc = new RemoveWriteOnGcInvokeClosure(rowId, rowTimestamp, rowLink, this);
+        RemoveWriteOnGcInvokeClosure removeWriteOnGc = new RemoveWriteOnGcInvokeClosure(
+                rowId,
+                rowTimestamp,
+                rowLink,
+                updateNextLinkHandler,
+                this
+        );
 
         try {
             renewableState.versionChainTree().invoke(new VersionChainKey(rowId), null, removeWriteOnGc);
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/BlobStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/BlobStorage.java
index 9bdfeb0..9e281f3 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/BlobStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/BlobStorage.java
@@ -305,7 +305,7 @@
     /**
      * Recycles a page and adds it to a {@link ReuseBag}.
      */
-    private class RecycleAndAddToReuseBag implements PageHandler<ReuseBag, Long> {
+    private static class RecycleAndAddToReuseBag implements PageHandler<ReuseBag, Long> {
         @Override
         public Long run(int groupId, long pageId, long page, long pageAddr, PageIo io, ReuseBag reuseBag, int unused,
                 IoStatisticsHolder statHolder) {
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/CommitWriteInvokeClosure.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/CommitWriteInvokeClosure.java
index c7d38c5..307c267 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/CommitWriteInvokeClosure.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/CommitWriteInvokeClosure.java
@@ -19,12 +19,19 @@
 
 import static org.apache.ignite.internal.pagememory.util.PageIdUtils.NULL_LINK;
 import static org.apache.ignite.internal.storage.pagememory.mv.AbstractPageMemoryMvPartitionStorage.DONT_LOAD_VALUE;
+import static org.apache.ignite.internal.util.GridUnsafe.pageSize;
 
 import org.apache.ignite.internal.hlc.HybridTimestamp;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
+import org.apache.ignite.internal.pagememory.freelist.FreeList;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
+import org.apache.ignite.internal.pagememory.io.PageIo;
+import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.OperationType;
+import org.apache.ignite.internal.pagememory.util.PageHandler;
 import org.apache.ignite.internal.pagememory.util.PageIdUtils;
 import org.apache.ignite.internal.storage.RowId;
 import org.apache.ignite.internal.storage.StorageException;
@@ -45,7 +52,7 @@
 
     private final AbstractPageMemoryMvPartitionStorage storage;
 
-    private final RowVersionFreeList rowVersionFreeList;
+    private final FreeList freeList;
 
     private final GcQueue gcQueue;
 
@@ -65,15 +72,54 @@
      */
     private long rowLinkForAddToGcQueue = NULL_LINK;
 
-    CommitWriteInvokeClosure(RowId rowId, HybridTimestamp timestamp, AbstractPageMemoryMvPartitionStorage storage) {
+    private final UpdateTimestampHandler updateTimestampHandler;
+
+    CommitWriteInvokeClosure(
+            RowId rowId,
+            HybridTimestamp timestamp,
+            UpdateTimestampHandler updateTimestampHandler,
+            AbstractPageMemoryMvPartitionStorage storage
+    ) {
         this.rowId = rowId;
         this.timestamp = timestamp;
         this.storage = storage;
+        this.updateTimestampHandler = updateTimestampHandler;
 
         RenewablePartitionStorageState localState = storage.renewableState;
 
-        this.rowVersionFreeList = localState.rowVersionFreeList();
+        this.freeList = localState.freeList();
         this.gcQueue = localState.gcQueue();
+
+    }
+
+    static class UpdateTimestampHandler implements PageHandler<HybridTimestamp, Object> {
+        private final PageEvictionTracker evictionTracker;
+
+        UpdateTimestampHandler(PageEvictionTracker evictionTracker) {
+            this.evictionTracker = evictionTracker;
+        }
+
+        @Override
+        public Object run(
+                int groupId,
+                long pageId,
+                long page,
+                long pageAddr,
+                PageIo io,
+                HybridTimestamp arg,
+                int itemId,
+                IoStatisticsHolder statHolder
+        ) throws IgniteInternalCheckedException {
+            DataPageIo dataIo = (DataPageIo) io;
+
+            int payloadOffset = dataIo.getPayloadOffset(pageAddr, itemId, pageSize(), 0);
+
+            HybridTimestamps.writeTimestampToMemory(pageAddr, payloadOffset + RowVersion.TIMESTAMP_OFFSET, arg);
+
+            evictionTracker.touchPage(pageId);
+
+            return true;
+        }
     }
 
     @Override
@@ -134,7 +180,7 @@
 
         if (updateTimestampLink != NULL_LINK) {
             try {
-                rowVersionFreeList.updateTimestamp(updateTimestampLink, timestamp);
+                freeList.updateDataRow(updateTimestampLink, updateTimestampHandler, timestamp);
             } catch (IgniteInternalCheckedException e) {
                 throw new StorageException(
                         "Error while update timestamp: [link={}, timestamp={}, {}]",
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/IndexStorageFactory.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/IndexStorageFactory.java
index 0bc40c0..b239c6c 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/IndexStorageFactory.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/IndexStorageFactory.java
@@ -24,14 +24,13 @@
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.pagememory.PageIdAllocator;
 import org.apache.ignite.internal.pagememory.PageMemory;
-import org.apache.ignite.internal.pagememory.reuse.ReuseList;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.util.PageLockListenerNoOp;
 import org.apache.ignite.internal.storage.StorageException;
 import org.apache.ignite.internal.storage.index.StorageHashIndexDescriptor;
 import org.apache.ignite.internal.storage.index.StorageIndexDescriptor;
 import org.apache.ignite.internal.storage.index.StorageSortedIndexDescriptor;
 import org.apache.ignite.internal.storage.pagememory.AbstractPageMemoryTableStorage;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.hash.HashIndexTree;
 import org.apache.ignite.internal.storage.pagememory.index.hash.PageMemoryHashIndexStorage;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMeta;
@@ -50,9 +49,7 @@
 
     private final IndexMetaTree indexMetaTree;
 
-    private final IndexColumnsFreeList indexFreeList;
-
-    private final ReuseList indexReuseList;
+    private final FreeListImpl freeList;
 
     @FunctionalInterface
     private interface IndexTreeConstructor<T> {
@@ -74,14 +71,12 @@
             AbstractPageMemoryTableStorage tableStorage,
             int partitionId,
             IndexMetaTree indexMetaTree,
-            IndexColumnsFreeList indexFreeList,
-            ReuseList indexReuseList
+            FreeListImpl freeList
     ) {
         this.tableStorage = tableStorage;
         this.partitionId = partitionId;
         this.indexMetaTree = indexMetaTree;
-        this.indexFreeList = indexFreeList;
-        this.indexReuseList = indexReuseList;
+        this.freeList = freeList;
     }
 
     /**
@@ -93,7 +88,7 @@
         return new PageMemoryHashIndexStorage(
                 treeAndMeta.indexMeta,
                 indexDescriptor,
-                indexFreeList,
+                freeList,
                 treeAndMeta.indexTree,
                 indexMetaTree,
                 tableStorage.isVolatile()
@@ -107,7 +102,7 @@
         return new PageMemoryHashIndexStorage(
                 indexMeta,
                 indexDescriptor,
-                indexFreeList,
+                freeList,
                 restoreHashIndexTree(indexMeta),
                 indexMetaTree,
                 tableStorage.isVolatile()
@@ -121,7 +116,7 @@
         return new PageMemoryHashIndexStorage(
                 indexMeta,
                 null,
-                indexFreeList,
+                freeList,
                 restoreHashIndexTree(indexMeta),
                 indexMetaTree,
                 tableStorage.isVolatile()
@@ -139,7 +134,7 @@
                         PageLockListenerNoOp.INSTANCE,
                         new AtomicLong(),
                         metaPageId,
-                        indexReuseList,
+                        freeList,
                         indexDescriptor
                 ));
     }
@@ -154,7 +149,7 @@
                     PageLockListenerNoOp.INSTANCE,
                     new AtomicLong(),
                     indexMeta.metaPageId(),
-                    indexReuseList
+                    freeList
             );
         } catch (IgniteInternalCheckedException e) {
             throw new StorageException(e);
@@ -170,7 +165,7 @@
         return new PageMemorySortedIndexStorage(
                 treeAndMeta.indexMeta,
                 indexDescriptor,
-                indexFreeList,
+                freeList,
                 treeAndMeta.indexTree,
                 indexMetaTree,
                 tableStorage.isVolatile()
@@ -184,7 +179,7 @@
         return new PageMemorySortedIndexStorage(
                 indexMeta,
                 indexDescriptor,
-                indexFreeList,
+                freeList,
                 restoreSortedIndexTree(indexDescriptor, indexMeta),
                 indexMetaTree,
                 tableStorage.isVolatile()
@@ -198,7 +193,7 @@
         return new PageMemorySortedIndexStorage(
                 indexMeta,
                 null,
-                indexFreeList,
+                freeList,
                 restoreSortedIndexTreeForDestroy(indexMeta),
                 indexMetaTree,
                 tableStorage.isVolatile()
@@ -216,7 +211,7 @@
                         PageLockListenerNoOp.INSTANCE,
                         new AtomicLong(),
                         metaPageId,
-                        indexReuseList,
+                        freeList,
                         indexDescriptor
                 )
         );
@@ -232,7 +227,7 @@
                     PageLockListenerNoOp.INSTANCE,
                     new AtomicLong(),
                     indexMeta.metaPageId(),
-                    indexReuseList,
+                    freeList,
                     indexDescriptor
             );
         } catch (IgniteInternalCheckedException e) {
@@ -250,7 +245,7 @@
                     PageLockListenerNoOp.INSTANCE,
                     new AtomicLong(),
                     indexMeta.metaPageId(),
-                    indexReuseList
+                    freeList
             );
         } catch (IgniteInternalCheckedException e) {
             throw new StorageException(e);
@@ -263,7 +258,7 @@
     void updateDataStructuresIn(PageMemoryHashIndexStorage indexStorage) {
         HashIndexTree indexTree = createHashIndexTreeAndMeta(indexStorage.indexDescriptor()).indexTree;
 
-        indexStorage.updateDataStructures(indexMetaTree, indexFreeList, indexTree);
+        indexStorage.updateDataStructures(indexMetaTree, freeList, indexTree);
     }
 
     /**
@@ -272,7 +267,7 @@
     void updateDataStructuresIn(PageMemorySortedIndexStorage indexStorage) {
         SortedIndexTree indexTree = createSortedIndexTreeAndMeta(indexStorage.indexDescriptor()).indexTree;
 
-        indexStorage.updateDataStructures(indexMetaTree, indexFreeList, indexTree);
+        indexStorage.updateDataStructures(indexMetaTree, freeList, indexTree);
     }
 
     private <T> IndexTreeAndMeta<T> createIndexTree(StorageIndexDescriptor descriptor, IndexTreeConstructor<T> treeConstructor) {
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageIoModule.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageIoModule.java
index dccc507..952ddf7 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageIoModule.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageIoModule.java
@@ -26,7 +26,6 @@
 import org.apache.ignite.internal.storage.pagememory.mv.gc.io.GcLeafIo;
 import org.apache.ignite.internal.storage.pagememory.mv.gc.io.GcMetaIo;
 import org.apache.ignite.internal.storage.pagememory.mv.io.BlobFragmentIo;
-import org.apache.ignite.internal.storage.pagememory.mv.io.RowVersionDataIo;
 import org.apache.ignite.internal.storage.pagememory.mv.io.VersionChainInnerIo;
 import org.apache.ignite.internal.storage.pagememory.mv.io.VersionChainLeafIo;
 import org.apache.ignite.internal.storage.pagememory.mv.io.VersionChainMetaIo;
@@ -43,7 +42,6 @@
                 VersionChainMetaIo.VERSIONS,
                 VersionChainInnerIo.VERSIONS,
                 VersionChainLeafIo.VERSIONS,
-                RowVersionDataIo.VERSIONS,
                 BlobFragmentIo.VERSIONS,
                 GcMetaIo.VERSIONS,
                 GcInnerIo.VERSIONS,
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageTypes.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageTypes.java
index 8b7d0f4..5679400 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageTypes.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/MvPageTypes.java
@@ -30,18 +30,15 @@
     /** Version chain tree leaf page IO type. */
     short T_VERSION_CHAIN_LEAF_IO = 11;
 
-    /** Row version data page IO type. */
-    short T_ROW_VERSION_DATA_IO = 12;
-
     /** Blob fragment page IO type. */
-    short T_BLOB_FRAGMENT_IO = 13;
+    short T_BLOB_FRAGMENT_IO = 12;
 
     /** Garbage collection queue meta page IO type. */
-    short T_GC_META_IO = 14;
+    short T_GC_META_IO = 13;
 
     /** Garbage collection queue inner page IO type. */
-    short T_GC_INNER_IO = 15;
+    short T_GC_INNER_IO = 14;
 
     /** Garbage collection queue leaf page IO type. */
-    short T_GC_LEAF_IO = 16;
+    short T_GC_LEAF_IO = 15;
 }
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java
index f7c802d..5911476 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/PersistentPageMemoryMvPartitionStorage.java
@@ -30,6 +30,8 @@
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.pagememory.DataRegion;
+import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolderNoOp;
 import org.apache.ignite.internal.pagememory.persistence.PartitionMeta;
 import org.apache.ignite.internal.pagememory.persistence.PersistentPageMemory;
@@ -46,8 +48,6 @@
 import org.apache.ignite.internal.storage.index.StorageSortedIndexDescriptor;
 import org.apache.ignite.internal.storage.pagememory.PersistentPageMemoryTableStorage;
 import org.apache.ignite.internal.storage.pagememory.configuration.schema.PersistentPageMemoryStorageEngineView;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumns;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.hash.PageMemoryHashIndexStorage;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.storage.pagememory.index.sorted.PageMemorySortedIndexStorage;
@@ -82,8 +82,8 @@
      * @param tableStorage Table storage.
      * @param partitionId Partition id.
      * @param meta Partition meta.
-     * @param rowVersionFreeList Free list for {@link RowVersion}.
-     * @param indexFreeList Free list fot {@link IndexColumns}.
+     * @param freeList Free list.
+     * @param pageEvictionTracker Page eviction tracker.
      * @param versionChainTree Table tree for {@link VersionChain}.
      * @param indexMetaTree Tree that contains SQL indexes' metadata.
      * @param gcQueue Garbage collection queue.
@@ -92,8 +92,8 @@
             PersistentPageMemoryTableStorage tableStorage,
             int partitionId,
             PartitionMeta meta,
-            RowVersionFreeList rowVersionFreeList,
-            IndexColumnsFreeList indexFreeList,
+            FreeListImpl freeList,
+            PageEvictionTracker pageEvictionTracker,
             VersionChainTree versionChainTree,
             IndexMetaTree indexMetaTree,
             GcQueue gcQueue,
@@ -102,12 +102,12 @@
         super(
                 partitionId,
                 tableStorage,
+                pageEvictionTracker,
                 new RenewablePartitionStorageState(
                         tableStorage,
                         partitionId,
                         versionChainTree,
-                        rowVersionFreeList,
-                        indexFreeList,
+                        freeList,
                         indexMetaTree,
                         gcQueue
                 ),
@@ -136,7 +136,7 @@
         }, dataRegion);
 
         blobStorage = new BlobStorage(
-                rowVersionFreeList,
+                freeList,
                 dataRegion.pageMemory(),
                 tableStorage.getTableId(),
                 partitionId,
@@ -361,8 +361,7 @@
 
         RenewablePartitionStorageState localState = renewableState;
 
-        resourcesToClose.add(localState.rowVersionFreeList()::close);
-        resourcesToClose.add(localState.indexFreeList()::close);
+        resourcesToClose.add(localState.freeList()::close);
         resourcesToClose.add(blobStorage::close);
 
         return resourcesToClose;
@@ -378,14 +377,10 @@
 
         if (executor == null) {
             busySafe(() -> {
-                saveRowVersionFreeListMetadataBusy(localState);
-
-                saveIndexFreeListMetadataBusy(localState);
+                saveFreeListMetadataBusy(localState);
             });
         } else {
-            executor.execute(() -> busySafe(() -> saveRowVersionFreeListMetadataBusy(localState)));
-
-            executor.execute(() -> busySafe(() -> saveIndexFreeListMetadataBusy(localState)));
+            executor.execute(() -> busySafe(() -> saveFreeListMetadataBusy(localState)));
         }
     }
 
@@ -400,8 +395,7 @@
      * Updates the internal data structures of the storage and its indexes on rebalance or cleanup.
      *
      * @param meta Partition meta.
-     * @param rowVersionFreeList Free list for {@link RowVersion}.
-     * @param indexFreeList Free list fot {@link IndexColumns}.
+     * @param freeList Free list.
      * @param versionChainTree Table tree for {@link VersionChain}.
      * @param indexMetaTree Tree that contains SQL indexes' metadata.
      * @param gcQueue Garbage collection queue.
@@ -409,8 +403,7 @@
      */
     public void updateDataStructures(
             PartitionMeta meta,
-            RowVersionFreeList rowVersionFreeList,
-            IndexColumnsFreeList indexFreeList,
+            FreeListImpl freeList,
             VersionChainTree versionChainTree,
             IndexMetaTree indexMetaTree,
             GcQueue gcQueue
@@ -420,7 +413,7 @@
         this.meta = meta;
 
         this.blobStorage = new BlobStorage(
-                rowVersionFreeList,
+                freeList,
                 tableStorage.dataRegion().pageMemory(),
                 tableStorage.getTableId(),
                 partitionId,
@@ -429,8 +422,7 @@
 
         updateRenewableState(
                 versionChainTree,
-                rowVersionFreeList,
-                indexFreeList,
+                freeList,
                 indexMetaTree,
                 gcQueue
         );
@@ -441,8 +433,7 @@
         RenewablePartitionStorageState localState = renewableState;
 
         return List.of(
-                localState.rowVersionFreeList()::close,
-                localState.indexFreeList()::close,
+                localState.freeList()::close,
                 localState.versionChainTree()::close,
                 localState.indexMetaTree()::close,
                 localState.gcQueue()::close,
@@ -457,19 +448,11 @@
         committedGroupConfigurationBusy(config);
     }
 
-    private void saveRowVersionFreeListMetadataBusy(RenewablePartitionStorageState localState) {
+    private void saveFreeListMetadataBusy(RenewablePartitionStorageState localState) {
         try {
-            localState.rowVersionFreeList().saveMetadata();
+            localState.freeList().saveMetadata();
         } catch (IgniteInternalCheckedException e) {
-            throw new StorageException("Failed to save RowVersionFreeList metadata: [{}]", e, createStorageInfo());
-        }
-    }
-
-    private void saveIndexFreeListMetadataBusy(RenewablePartitionStorageState localState) {
-        try {
-            localState.indexFreeList().saveMetadata();
-        } catch (IgniteInternalCheckedException e) {
-            throw new StorageException("Failed to save IndexColumnsFreeList metadata: [{}]", e, createStorageInfo());
+            throw new StorageException("Failed to save free list metadata: [{}]", e, createStorageInfo());
         }
     }
 }
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/ReadRowVersionValue.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/ReadRowVersionValue.java
index 6d4a915..9d24b11 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/ReadRowVersionValue.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/ReadRowVersionValue.java
@@ -34,4 +34,9 @@
     protected int valueOffsetInFirstSlot() {
         return RowVersion.VALUE_OFFSET;
     }
+
+    @Override
+    protected byte dataType() {
+        return RowVersion.DATA_TYPE;
+    }
 }
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RemoveWriteOnGcInvokeClosure.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RemoveWriteOnGcInvokeClosure.java
index d1ac224..c27fb46 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RemoveWriteOnGcInvokeClosure.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RemoveWriteOnGcInvokeClosure.java
@@ -18,16 +18,24 @@
 package org.apache.ignite.internal.storage.pagememory.mv;
 
 import static org.apache.ignite.internal.pagememory.util.PageIdUtils.NULL_LINK;
+import static org.apache.ignite.internal.pagememory.util.PartitionlessLinks.writePartitionless;
 import static org.apache.ignite.internal.storage.pagememory.mv.AbstractPageMemoryMvPartitionStorage.ALWAYS_LOAD_VALUE;
 import static org.apache.ignite.internal.storage.pagememory.mv.AbstractPageMemoryMvPartitionStorage.DONT_LOAD_VALUE;
 import static org.apache.ignite.internal.storage.pagememory.mv.FindRowVersion.RowVersionFilter.equalsByNextLink;
+import static org.apache.ignite.internal.util.GridUnsafe.pageSize;
 
 import java.util.List;
 import org.apache.ignite.internal.hlc.HybridTimestamp;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
+import org.apache.ignite.internal.pagememory.freelist.FreeList;
+import org.apache.ignite.internal.pagememory.io.DataPageIo;
+import org.apache.ignite.internal.pagememory.io.PageIo;
+import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure;
 import org.apache.ignite.internal.pagememory.tree.IgniteTree.OperationType;
+import org.apache.ignite.internal.pagememory.util.PageHandler;
 import org.apache.ignite.internal.storage.RowId;
 import org.apache.ignite.internal.storage.StorageException;
 import org.apache.ignite.internal.storage.gc.GcEntry;
@@ -51,7 +59,7 @@
 
     private final AbstractPageMemoryMvPartitionStorage storage;
 
-    private final RowVersionFreeList rowVersionFreeList;
+    private final FreeList freeList;
 
     private final GcQueue gcQueue;
 
@@ -67,7 +75,15 @@
 
     private @Nullable RowVersion toDropFromQueue;
 
-    RemoveWriteOnGcInvokeClosure(RowId rowId, HybridTimestamp timestamp, long link, AbstractPageMemoryMvPartitionStorage storage) {
+    private final UpdateNextLinkHandler updateNextLinkHandler;
+
+    RemoveWriteOnGcInvokeClosure(
+            RowId rowId,
+            HybridTimestamp timestamp,
+            long link,
+            UpdateNextLinkHandler updateNextLinkHandler,
+            AbstractPageMemoryMvPartitionStorage storage
+    ) {
         this.rowId = rowId;
         this.timestamp = timestamp;
         this.link = link;
@@ -75,8 +91,40 @@
 
         RenewablePartitionStorageState localState = storage.renewableState;
 
-        this.rowVersionFreeList = localState.rowVersionFreeList();
+        this.freeList = localState.freeList();
         this.gcQueue = localState.gcQueue();
+
+        this.updateNextLinkHandler = updateNextLinkHandler;
+    }
+
+    static class UpdateNextLinkHandler implements PageHandler<Long, Object> {
+        private final PageEvictionTracker evictionTracker;
+
+        UpdateNextLinkHandler(PageEvictionTracker evictionTracker) {
+            this.evictionTracker = evictionTracker;
+        }
+
+        @Override
+        public Object run(
+                int groupId,
+                long pageId,
+                long page,
+                long pageAddr,
+                PageIo io,
+                Long nextLink,
+                int itemId,
+                IoStatisticsHolder statHolder
+        ) throws IgniteInternalCheckedException {
+            DataPageIo dataIo = (DataPageIo) io;
+
+            int payloadOffset = dataIo.getPayloadOffset(pageAddr, itemId, pageSize(), 0);
+
+            writePartitionless(pageAddr + payloadOffset + RowVersion.NEXT_LINK_OFFSET, nextLink);
+
+            evictionTracker.touchPage(pageId);
+
+            return true;
+        }
     }
 
     @Override
@@ -151,7 +199,7 @@
     public void onUpdate() {
         if (toUpdate != null) {
             try {
-                rowVersionFreeList.updateNextLink(toUpdate.link(), NULL_LINK);
+                updateNextLink(toUpdate.link(), NULL_LINK);
             } catch (IgniteInternalCheckedException e) {
                 throw new StorageException(
                         "Error updating the next link: [rowId={}, timestamp={}, rowLink={}, nextLink={}, {}]",
@@ -162,6 +210,10 @@
         }
     }
 
+    private void updateNextLink(long link, long nextLink) throws IgniteInternalCheckedException {
+        freeList.updateDataRow(link, updateNextLinkHandler, nextLink);
+    }
+
     private RowVersion readRowVersionWithChecks(VersionChain versionChain) {
         RowVersion rowVersion = storage.readRowVersion(link, DONT_LOAD_VALUE);
 
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RenewablePartitionStorageState.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RenewablePartitionStorageState.java
index 5f0d18a..ccff1b3 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RenewablePartitionStorageState.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RenewablePartitionStorageState.java
@@ -17,8 +17,8 @@
 
 package org.apache.ignite.internal.storage.pagememory.mv;
 
+import org.apache.ignite.internal.pagememory.freelist.FreeListImpl;
 import org.apache.ignite.internal.storage.pagememory.AbstractPageMemoryTableStorage;
-import org.apache.ignite.internal.storage.pagememory.index.freelist.IndexColumnsFreeList;
 import org.apache.ignite.internal.storage.pagememory.index.meta.IndexMetaTree;
 import org.apache.ignite.internal.storage.pagememory.mv.gc.GcQueue;
 
@@ -29,9 +29,7 @@
 class RenewablePartitionStorageState {
     private final VersionChainTree versionChainTree;
 
-    private final RowVersionFreeList rowVersionFreeList;
-
-    private final IndexColumnsFreeList indexFreeList;
+    private final FreeListImpl freeList;
 
     private final IndexMetaTree indexMetaTree;
 
@@ -44,14 +42,12 @@
             AbstractPageMemoryTableStorage tableStorage,
             int partitionId,
             VersionChainTree versionChainTree,
-            RowVersionFreeList rowVersionFreeList,
-            IndexColumnsFreeList indexFreeList,
+            FreeListImpl freeList,
             IndexMetaTree indexMetaTree,
             GcQueue gcQueue
     ) {
         this.versionChainTree = versionChainTree;
-        this.rowVersionFreeList = rowVersionFreeList;
-        this.indexFreeList = indexFreeList;
+        this.freeList = freeList;
         this.indexMetaTree = indexMetaTree;
         this.gcQueue = gcQueue;
 
@@ -59,8 +55,7 @@
                 tableStorage,
                 partitionId,
                 indexMetaTree,
-                indexFreeList,
-                rowVersionFreeList
+                freeList
         );
     }
 
@@ -68,12 +63,8 @@
         return versionChainTree;
     }
 
-    RowVersionFreeList rowVersionFreeList() {
-        return rowVersionFreeList;
-    }
-
-    IndexColumnsFreeList indexFreeList() {
-        return indexFreeList;
+    FreeListImpl freeList() {
+        return freeList;
     }
 
     IndexMetaTree indexMetaTree() {
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RowVersion.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RowVersion.java
index e849393..7e5a933 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RowVersion.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RowVersion.java
@@ -19,14 +19,14 @@
 
 import static org.apache.ignite.internal.hlc.HybridTimestamp.HYBRID_TIMESTAMP_SIZE;
 import static org.apache.ignite.internal.pagememory.util.PageIdUtils.NULL_LINK;
+import static org.apache.ignite.internal.pagememory.util.PartitionlessLinks.writePartitionless;
 
+import java.nio.ByteBuffer;
 import org.apache.ignite.internal.hlc.HybridTimestamp;
 import org.apache.ignite.internal.pagememory.Storable;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
-import org.apache.ignite.internal.pagememory.io.IoVersions;
+import org.apache.ignite.internal.pagememory.util.PageUtils;
 import org.apache.ignite.internal.pagememory.util.PartitionlessLinks;
 import org.apache.ignite.internal.schema.BinaryRow;
-import org.apache.ignite.internal.storage.pagememory.mv.io.RowVersionDataIo;
 import org.apache.ignite.internal.tostring.IgniteToStringExclude;
 import org.apache.ignite.internal.tostring.S;
 import org.jetbrains.annotations.Nullable;
@@ -35,11 +35,11 @@
  * Represents row version inside row version chain.
  */
 public final class RowVersion implements Storable {
+    public static final byte DATA_TYPE = 0;
     private static final int NEXT_LINK_STORE_SIZE_BYTES = PartitionlessLinks.PARTITIONLESS_LINK_SIZE_BYTES;
     private static final int VALUE_SIZE_STORE_SIZE_BYTES = Integer.BYTES;
     private static final int SCHEMA_VERSION_SIZE_BYTES = Short.BYTES;
-
-    public static final int TIMESTAMP_OFFSET = 0;
+    public static final int TIMESTAMP_OFFSET = DATA_TYPE_OFFSET + DATA_TYPE_SIZE_BYTES;
     public static final int NEXT_LINK_OFFSET = TIMESTAMP_OFFSET + HYBRID_TIMESTAMP_SIZE;
     public static final int VALUE_SIZE_OFFSET = NEXT_LINK_OFFSET + NEXT_LINK_STORE_SIZE_BYTES;
     public static final int SCHEMA_VERSION_OFFSET = VALUE_SIZE_OFFSET + VALUE_SIZE_STORE_SIZE_BYTES;
@@ -155,12 +155,67 @@
 
     @Override
     public int headerSize() {
-        return HYBRID_TIMESTAMP_SIZE + NEXT_LINK_STORE_SIZE_BYTES + VALUE_SIZE_STORE_SIZE_BYTES + SCHEMA_VERSION_SIZE_BYTES;
+        return HYBRID_TIMESTAMP_SIZE + DATA_TYPE_SIZE_BYTES + NEXT_LINK_STORE_SIZE_BYTES + VALUE_SIZE_STORE_SIZE_BYTES
+                + SCHEMA_VERSION_SIZE_BYTES;
     }
 
     @Override
-    public IoVersions<? extends AbstractDataPageIo<?>> ioVersions() {
-        return RowVersionDataIo.VERSIONS;
+    public void writeRowData(long pageAddr, int dataOff, int payloadSize, boolean newRow) {
+        PageUtils.putShort(pageAddr, dataOff, (short) payloadSize);
+        dataOff += Short.BYTES;
+
+        PageUtils.putByte(pageAddr, dataOff + DATA_TYPE_OFFSET, DATA_TYPE);
+
+        HybridTimestamps.writeTimestampToMemory(pageAddr, dataOff + TIMESTAMP_OFFSET, timestamp());
+
+        writePartitionless(pageAddr + dataOff + NEXT_LINK_OFFSET, nextLink());
+
+        PageUtils.putInt(pageAddr, dataOff + VALUE_SIZE_OFFSET, valueSize());
+
+        if (value != null) {
+            PageUtils.putShort(pageAddr, dataOff + SCHEMA_VERSION_OFFSET, (short) value.schemaVersion());
+
+            PageUtils.putByteBuffer(pageAddr, dataOff + VALUE_OFFSET, value.tupleSlice());
+        } else {
+            PageUtils.putShort(pageAddr, dataOff + SCHEMA_VERSION_OFFSET, (short) 0);
+        }
+    }
+
+    @Override
+    public void writeFragmentData(ByteBuffer pageBuf, int rowOff, int payloadSize) {
+        int headerSize = headerSize();
+
+        int bufferOffset;
+        int bufferSize;
+
+        if (rowOff == 0) {
+            // first fragment
+            assert headerSize <= payloadSize : "Header must entirely fit in the first fragment, but header size is "
+                    + headerSize + " and payload size is " + payloadSize;
+
+            pageBuf.put(DATA_TYPE);
+
+            HybridTimestamps.writeTimestampToBuffer(pageBuf, timestamp());
+
+            PartitionlessLinks.writeToBuffer(pageBuf, nextLink());
+
+            pageBuf.putInt(valueSize());
+
+            pageBuf.putShort(value == null ? 0 : (short) value.schemaVersion());
+
+            bufferOffset = 0;
+            bufferSize = payloadSize - headerSize;
+        } else {
+            // non-first fragment
+            assert rowOff >= headerSize;
+
+            bufferOffset = rowOff - headerSize;
+            bufferSize = payloadSize;
+        }
+
+        if (value != null) {
+            Storable.putValueBufferIntoPage(pageBuf, value.tupleSlice(), bufferOffset, bufferSize);
+        }
     }
 
     @Override
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RowVersionFreeList.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RowVersionFreeList.java
deleted file mode 100644
index 6026595..0000000
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/RowVersionFreeList.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.storage.pagememory.mv;
-
-import java.util.concurrent.atomic.AtomicLong;
-import org.apache.ignite.internal.hlc.HybridTimestamp;
-import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
-import org.apache.ignite.internal.logger.IgniteLogger;
-import org.apache.ignite.internal.logger.Loggers;
-import org.apache.ignite.internal.pagememory.PageMemory;
-import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
-import org.apache.ignite.internal.pagememory.freelist.AbstractFreeList;
-import org.apache.ignite.internal.pagememory.io.PageIo;
-import org.apache.ignite.internal.pagememory.metric.IoStatisticsHolder;
-import org.apache.ignite.internal.pagememory.reuse.ReuseList;
-import org.apache.ignite.internal.pagememory.util.PageHandler;
-import org.apache.ignite.internal.pagememory.util.PageLockListener;
-import org.apache.ignite.internal.storage.pagememory.mv.io.RowVersionDataIo;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * {@link AbstractFreeList} for {@link RowVersion} instances.
- */
-public class RowVersionFreeList extends AbstractFreeList<RowVersion> {
-    private static final IgniteLogger LOG = Loggers.forClass(RowVersionFreeList.class);
-
-    private final IoStatisticsHolder statHolder;
-
-    private final UpdateTimestampHandler updateTimestampHandler = new UpdateTimestampHandler();
-
-    private final UpdateNextLinkHandler updateNextLinkHandler = new UpdateNextLinkHandler();
-
-    /**
-     * Constructor.
-     *
-     * @param grpId Group ID.
-     * @param partId Partition ID.
-     * @param pageMem Page memory.
-     * @param reuseList Reuse list to track pages that can be reused after they get completely empty (if {@code null}, the free list itself
-     *      will be used as a ReuseList.
-     * @param lockLsnr Page lock listener.
-     * @param metaPageId Metadata page ID.
-     * @param initNew {@code True} if new metadata should be initialized.
-     * @param pageListCacheLimit Page list cache limit.
-     * @param evictionTracker Page eviction tracker.
-     * @param statHolder Statistics holder to track IO operations.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public RowVersionFreeList(
-            int grpId,
-            int partId,
-            PageMemory pageMem,
-            @Nullable ReuseList reuseList,
-            PageLockListener lockLsnr,
-            long metaPageId,
-            boolean initNew,
-            @Nullable AtomicLong pageListCacheLimit,
-            PageEvictionTracker evictionTracker,
-            IoStatisticsHolder statHolder
-    ) throws IgniteInternalCheckedException {
-        super(
-                grpId,
-                partId,
-                "RowVersionFreeList_" + grpId,
-                pageMem,
-                reuseList,
-                lockLsnr,
-                LOG,
-                metaPageId,
-                initNew,
-                pageListCacheLimit,
-                evictionTracker
-        );
-
-        this.statHolder = statHolder;
-    }
-
-    /**
-     * Inserts a row.
-     *
-     * @param row Row.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public void insertDataRow(RowVersion row) throws IgniteInternalCheckedException {
-        super.insertDataRow(row, statHolder);
-    }
-
-    /**
-     * Updates row version's timestamp.
-     *
-     * @param link link to the slot containing row version
-     * @param newTimestamp timestamp to set
-     * @throws IgniteInternalCheckedException if something fails
-     */
-    public void updateTimestamp(long link, HybridTimestamp newTimestamp) throws IgniteInternalCheckedException {
-        updateDataRow(link, updateTimestampHandler, newTimestamp, statHolder);
-    }
-
-    /**
-     * Updates row version's next link.
-     *
-     * @param link Row version link.
-     * @param nextLink Next row version link to set.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public void updateNextLink(long link, long nextLink) throws IgniteInternalCheckedException {
-        updateDataRow(link, updateNextLinkHandler, nextLink, statHolder);
-    }
-
-    /**
-     * Removes a row by link.
-     *
-     * @param link Row link.
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public void removeDataRowByLink(long link) throws IgniteInternalCheckedException {
-        super.removeDataRowByLink(link, statHolder);
-    }
-
-    private class UpdateTimestampHandler implements PageHandler<HybridTimestamp, Object> {
-        @Override
-        public Object run(
-                int groupId,
-                long pageId,
-                long page,
-                long pageAddr,
-                PageIo io,
-                HybridTimestamp arg,
-                int itemId,
-                IoStatisticsHolder statHolder
-        ) throws IgniteInternalCheckedException {
-            RowVersionDataIo dataIo = (RowVersionDataIo) io;
-
-            dataIo.updateTimestamp(pageAddr, itemId, pageSize(), arg);
-
-            evictionTracker.touchPage(pageId);
-
-            return true;
-        }
-    }
-
-    private class UpdateNextLinkHandler implements PageHandler<Long, Object> {
-        @Override
-        public Object run(
-                int groupId,
-                long pageId,
-                long page,
-                long pageAddr,
-                PageIo io,
-                Long nextLink,
-                int itemId,
-                IoStatisticsHolder statHolder
-        ) throws IgniteInternalCheckedException {
-            RowVersionDataIo dataIo = (RowVersionDataIo) io;
-
-            dataIo.updateNextLink(pageAddr, itemId, pageSize(), nextLink);
-
-            evictionTracker.touchPage(pageId);
-
-            return true;
-        }
-    }
-
-    /**
-     * Shortcut method for {@link #saveMetadata(IoStatisticsHolder)} with statistics holder.
-     *
-     * @throws IgniteInternalCheckedException If failed.
-     */
-    public void saveMetadata() throws IgniteInternalCheckedException {
-        super.saveMetadata(statHolder);
-    }
-}
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java
index a44c904..bbe8b11 100644
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java
+++ b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/VolatilePageMemoryMvPartitionStorage.java
@@ -34,6 +34,7 @@
 import org.apache.ignite.internal.lang.IgniteInternalException;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.pagememory.evict.PageEvictionTracker;
 import org.apache.ignite.internal.pagememory.tree.BplusTree;
 import org.apache.ignite.internal.pagememory.util.GradualTask;
 import org.apache.ignite.internal.pagememory.util.GradualTaskExecutor;
@@ -72,6 +73,7 @@
      *
      * @param tableStorage Table storage instance.
      * @param partitionId Partition id.
+     * @param pageEvictionTracker Page eviction tracker.
      * @param versionChainTree Table tree for {@link VersionChain}.
      * @param indexMetaTree Tree that contains SQL indexes' metadata.
      * @param destructionExecutor Executor used to destruct partitions.
@@ -80,6 +82,7 @@
     public VolatilePageMemoryMvPartitionStorage(
             VolatilePageMemoryTableStorage tableStorage,
             int partitionId,
+            PageEvictionTracker pageEvictionTracker,
             VersionChainTree versionChainTree,
             IndexMetaTree indexMetaTree,
             GcQueue gcQueue,
@@ -88,12 +91,12 @@
         super(
                 partitionId,
                 tableStorage,
+                pageEvictionTracker,
                 new RenewablePartitionStorageState(
                         tableStorage,
                         partitionId,
                         versionChainTree,
-                        tableStorage.dataRegion().rowVersionFreeList(),
-                        tableStorage.dataRegion().indexColumnsFreeList(),
+                        tableStorage.dataRegion().freeList(),
                         indexMetaTree,
                         gcQueue
                 ),
@@ -272,7 +275,7 @@
         while (rowVersionLink != PageIdUtils.NULL_LINK) {
             RowVersion rowVersion = readRowVersion(rowVersionLink, NEVER_LOAD_VALUE);
 
-            renewableState.rowVersionFreeList().removeDataRowByLink(rowVersion.link());
+            renewableState.freeList().removeDataRowByLink(rowVersion.link());
 
             rowVersionLink = rowVersion.nextLink();
         }
@@ -340,8 +343,7 @@
 
         updateRenewableState(
                 versionChainTree,
-                prevState.rowVersionFreeList(),
-                prevState.indexFreeList(),
+                prevState.freeList(),
                 indexMetaTree,
                 gcQueue
         );
diff --git a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/io/RowVersionDataIo.java b/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/io/RowVersionDataIo.java
deleted file mode 100644
index 2aa7fd6..0000000
--- a/modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/mv/io/RowVersionDataIo.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.storage.pagememory.mv.io;
-
-import static org.apache.ignite.internal.pagememory.util.PageUtils.putByteBuffer;
-import static org.apache.ignite.internal.pagememory.util.PageUtils.putInt;
-import static org.apache.ignite.internal.pagememory.util.PageUtils.putShort;
-import static org.apache.ignite.internal.pagememory.util.PartitionlessLinks.writePartitionless;
-import static org.apache.ignite.internal.storage.pagememory.mv.MvPageTypes.T_ROW_VERSION_DATA_IO;
-
-import java.nio.ByteBuffer;
-import org.apache.ignite.internal.hlc.HybridTimestamp;
-import org.apache.ignite.internal.lang.IgniteStringBuilder;
-import org.apache.ignite.internal.pagememory.io.AbstractDataPageIo;
-import org.apache.ignite.internal.pagememory.io.IoVersions;
-import org.apache.ignite.internal.pagememory.util.PartitionlessLinks;
-import org.apache.ignite.internal.schema.BinaryRow;
-import org.apache.ignite.internal.storage.pagememory.mv.HybridTimestamps;
-import org.apache.ignite.internal.storage.pagememory.mv.RowVersion;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Data pages IO for {@link RowVersion}.
- */
-public class RowVersionDataIo extends AbstractDataPageIo<RowVersion> {
-    /** I/O versions. */
-    public static final IoVersions<RowVersionDataIo> VERSIONS = new IoVersions<>(new RowVersionDataIo(1));
-
-    /**
-     * Constructor.
-     *
-     * @param ver Page format version.
-     */
-    protected RowVersionDataIo(int ver) {
-        super(T_ROW_VERSION_DATA_IO, ver);
-    }
-
-    @Override
-    protected void writeRowData(long pageAddr, int dataOff, int payloadSize, RowVersion rowVersion, boolean newRow) {
-        assertPageType(pageAddr);
-
-        int offset = dataOff;
-
-        putShort(pageAddr, offset, (short) payloadSize);
-        offset += Short.BYTES;
-
-        offset += HybridTimestamps.writeTimestampToMemory(pageAddr, offset, rowVersion.timestamp());
-
-        offset += writePartitionless(pageAddr + offset, rowVersion.nextLink());
-
-        putInt(pageAddr, offset, rowVersion.valueSize());
-        offset += Integer.BYTES;
-
-        BinaryRow row = rowVersion.value();
-
-        if (row != null) {
-            putShort(pageAddr, offset, (short) row.schemaVersion());
-            offset += Short.BYTES;
-
-            putByteBuffer(pageAddr, offset, row.tupleSlice());
-        } else {
-            putShort(pageAddr, offset, (short) 0);
-        }
-    }
-
-    @Override
-    protected void writeFragmentData(RowVersion rowVersion, ByteBuffer pageBuf, int rowOff, int payloadSize) {
-        assertPageType(pageBuf);
-
-        int headerSize = rowVersion.headerSize();
-
-        BinaryRow row = rowVersion.value();
-
-        int bufferOffset;
-        int bufferSize;
-
-        if (rowOff == 0) {
-            // first fragment
-            assert headerSize <= payloadSize : "Header must entirely fit in the first fragment, but header size is "
-                    + headerSize + " and payload size is " + payloadSize;
-
-            HybridTimestamps.writeTimestampToBuffer(pageBuf, rowVersion.timestamp());
-
-            PartitionlessLinks.writeToBuffer(pageBuf, rowVersion.nextLink());
-
-            pageBuf.putInt(rowVersion.valueSize());
-
-            pageBuf.putShort(row == null ? 0 : (short) row.schemaVersion());
-
-            bufferOffset = 0;
-            bufferSize = payloadSize - headerSize;
-        } else {
-            // non-first fragment
-            assert rowOff >= headerSize;
-
-            bufferOffset = rowOff - headerSize;
-            bufferSize = payloadSize;
-        }
-
-        if (row != null) {
-            putValueBufferIntoPage(pageBuf, row.tupleSlice(), bufferOffset, bufferSize);
-        }
-    }
-
-    /**
-     * Updates timestamp leaving the rest untouched.
-     *
-     * @param pageAddr  page address
-     * @param itemId    item ID of the slot where row version (or its first fragment) is stored in this page
-     * @param pageSize  size of the page
-     * @param timestamp timestamp to store
-     */
-    public void updateTimestamp(long pageAddr, int itemId, int pageSize, @Nullable HybridTimestamp timestamp) {
-        int payloadOffset = getPayloadOffset(pageAddr, itemId, pageSize, 0);
-
-        HybridTimestamps.writeTimestampToMemory(pageAddr, payloadOffset + RowVersion.TIMESTAMP_OFFSET, timestamp);
-    }
-
-    /**
-     * Updates next link leaving the rest untouched.
-     *
-     * @param pageAddr Page address.
-     * @param itemId Item ID of the slot where row version (or its first fragment) is stored in this page.
-     * @param pageSize Size of the page.
-     * @param nextLink Next link to store.
-     */
-    public void updateNextLink(long pageAddr, int itemId, int pageSize, long nextLink) {
-        int payloadOffset = getPayloadOffset(pageAddr, itemId, pageSize, 0);
-
-        writePartitionless(pageAddr + payloadOffset + RowVersion.NEXT_LINK_OFFSET, nextLink);
-    }
-
-    @Override
-    protected void printPage(long addr, int pageSize, IgniteStringBuilder sb) {
-        sb.app("RowVersionDataIo [\n");
-        printPageLayout(addr, pageSize, sb);
-        sb.app("\n]");
-    }
-}
diff --git a/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryMvTableStorageTest.java b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryMvTableStorageTest.java
index c1aaec1..6043842 100644
--- a/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryMvTableStorageTest.java
+++ b/modules/storage-page-memory/src/test/java/org/apache/ignite/internal/storage/pagememory/VolatilePageMemoryMvTableStorageTest.java
@@ -102,7 +102,7 @@
 
         insertOneRow(partitionStorage);
 
-        long emptyDataPagesBeforeDestroy = dataRegion().rowVersionFreeList().emptyDataPages();
+        long emptyDataPagesBeforeDestroy = dataRegion().freeList().emptyDataPages();
 
         assertThat(tableStorage.destroyPartition(0), willSucceedFast());
 
@@ -112,7 +112,7 @@
     private void assertMvDataDestructionCompletes(long emptyDataPagesBeforeDestroy)
             throws InterruptedException, IgniteInternalCheckedException {
         assertTrue(waitForCondition(
-                () -> dataRegion().rowVersionFreeList().emptyDataPages() > emptyDataPagesBeforeDestroy,
+                () -> dataRegion().freeList().emptyDataPages() > emptyDataPagesBeforeDestroy,
                 5_000
         ));
 
@@ -141,7 +141,7 @@
 
         insertOneRow(partitionStorage);
 
-        long emptyDataPagesBeforeDestroy = dataRegion().rowVersionFreeList().emptyDataPages();
+        long emptyDataPagesBeforeDestroy = dataRegion().freeList().emptyDataPages();
 
         assertThat(tableStorage.destroy(), willSucceedFast());
 
@@ -156,8 +156,7 @@
 
         indexStorage.put(nonInlinableIndexRow());
 
-        // Using RowVersionFreeList to track removal because RowVersionFreeList is used as a ReuseList for IndexColumnsFreeList.
-        long emptyIndexPagesBeforeDestroy = dataRegion().rowVersionFreeList().emptyDataPages();
+        long emptyIndexPagesBeforeDestroy = dataRegion().freeList().emptyDataPages();
 
         assertThat(tableStorage.destroyPartition(0), willSucceedFast());
 
@@ -184,9 +183,8 @@
 
     private void assertIndexDataDestructionCompletes(long emptyIndexPagesBeforeDestroy)
             throws InterruptedException, IgniteInternalCheckedException {
-        // Using RowVersionFreeList to track removal because RowVersionFreeList is used as a ReuseList for IndexColumnsFreeList.
         assertTrue(waitForCondition(
-                () -> dataRegion().rowVersionFreeList().emptyDataPages() > emptyIndexPagesBeforeDestroy,
+                () -> dataRegion().freeList().emptyDataPages() > emptyIndexPagesBeforeDestroy,
                 5_000
         ));
 
@@ -203,8 +201,7 @@
 
         indexStorage.put(nonInlinableIndexRow());
 
-        // Using RowVersionFreeList to track removal because RowVersionFreeList is used as a ReuseList for IndexColumnsFreeList.
-        long emptyIndexPagesBeforeDestroy = dataRegion().rowVersionFreeList().emptyDataPages();
+        long emptyIndexPagesBeforeDestroy = dataRegion().freeList().emptyDataPages();
 
         assertThat(tableStorage.destroyPartition(0), willSucceedFast());
 
@@ -219,8 +216,7 @@
 
         indexStorage.put(nonInlinableIndexRow());
 
-        // Using RowVersionFreeList to track removal because RowVersionFreeList is used as a ReuseList for IndexColumnsFreeList.
-        long emptyIndexPagesBeforeDestroy = dataRegion().rowVersionFreeList().emptyDataPages();
+        long emptyIndexPagesBeforeDestroy = dataRegion().freeList().emptyDataPages();
 
         assertThat(tableStorage.destroy(), willSucceedFast());
 
@@ -235,8 +231,7 @@
 
         indexStorage.put(nonInlinableIndexRow());
 
-        // Using RowVersionFreeList to track removal because RowVersionFreeList is used as a ReuseList for IndexColumnsFreeList.
-        long emptyIndexPagesBeforeDestroy = dataRegion().rowVersionFreeList().emptyDataPages();
+        long emptyIndexPagesBeforeDestroy = dataRegion().freeList().emptyDataPages();
 
         assertThat(tableStorage.destroy(), willSucceedFast());