You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2023/06/28 09:48:17 UTC

[jena] branch main updated: GH-1912: GraphMem2 New in-memory, general-purpose, non-transactional graphs as successors of GraphMem: All variants strictly use term-equality and do not support Iterator#remove. (GraphMem uses value-equality for object nodes)

This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new c65e4e3706 GH-1912: GraphMem2 New in-memory, general-purpose, non-transactional graphs as successors of GraphMem: All variants strictly use term-equality and do not support Iterator#remove. (GraphMem uses value-equality for object nodes)
     new a8473636e5 Merge pull request #1918 from arne-bdt/GH1912_GraphMem2
c65e4e3706 is described below

commit c65e4e37062bd1d104ad46e365385a7d3e556d68
Author: Arne Bernhardt <ar...@soptim.de>
AuthorDate: Fri Jun 23 01:23:05 2023 +0200

    GH-1912: GraphMem2
    New in-memory, general-purpose, non-transactional graphs as successors of GraphMem:
    All variants strictly use term-equality and do not support Iterator#remove.
    (GraphMem uses value-equality for object nodes)
    
    GraphMem2Legacy:
    - Purpose: Use this graph implementation if you want to maintain the 'old' behavior of GraphMem or if your memory constraints
      prevent you from utilizing more memory-intensive solutions.
    - Slightly improved performance compared to GraphMem
    - Simplified implementation, primarily due to lack of support for Iterator#remove
    - The heritage of GraphMem:
      - Same basic structure
      - Same memory consumption
    - Also based on HashCommon
    
    GraphMem2Fast:
    - Purpose: GraphMem2Fast is a strong candidate for becoming the new default in-memory graph in the upcoming Jena 5, thanks to
      its improved performance and relatively minor increase in memory usage.
    - Faster than GraphMem2Legacy (specially Graph#add, Graph#find and Graph#stream)
    - Memory consumption is about 6-35% higher than GraphMem2Legacy
    - Maps and sets are not based on HashCommon, but use a faster custom alternative (only #remove is a bit slower)
    - Benefits from multiple small optimizations
    - The heritage of GraphMem:
      - Also uses 3 hash-maps indexed by subjects, predicates, and objects
      - Values of the maps also switch from arrays to hash sets for the triples
    
    GraphMem2Roaring
    - Purpose: GraphMem2Roaring is ideal for handling extremely large graphs. If you frequently work with such massive data
      structures, this implementation could be your top choice.
    - Graph#contains is faster than GraphMem2Fast
    - Better performance than GraphMem2Fast for operations with triple matches for the pattern S_O, SP_, and _PO on large graphs,
      due to bit-operations to find intersecting triples
    - Memory consumption is about 7-99% higher than GraphMem2Legacy
    - Suitable for really large graphs like bsbm-5m.nt.gz, bsbm-25m.nt.gz, and possibly even larger
    - Simple and straightforward implementation
    - No heritage of GraphMem
    - Internal structure:
      - One indexed hash set (same as GraphMem2Fast uses) that holds all triples
      - Three hash maps indexed by subjects, predicates, and objects with RoaringBitmaps as values
      - The bitmaps contain the indices of the triples in the central hash set
    
    Other Changes:
    - org.apache.jena.graph.test.TestGraph
      - added GraphMem2Fast, GraphMem2Legacy and GraphMem2Roaring to the suite
    - GraphMem:
      - moved property "TripleStore store" from GraphMemBase to GraphMem
        --> needed this to make a clean GraphMem2, which also extends GraphMem but the TripleStore interface is slightly different.
    - pom.xml:
      - added dependency roaringbitmap 0.9.44
    - jena-benchmarks-jmh
      - added the three new graph implementations to the benchmarks
      - randomized the order of test data in some benchmarks to prevent them from showing order dependent behaviour
      - added benchmarks for sets and maps comparing
        - HashCommonSet vs. FastHashSet vs. Java HashSet
        - HashCommonMap vs. FastHashMap vs. Java HashMap
---
 .../org/apache/jena/mem/graph/TestGraphAdd.java    |    8 +-
 .../jena/mem/graph/TestGraphContainsAnything.java  |   77 +-
 .../jena/mem/graph/TestGraphContainsTriple.java    |   67 +-
 .../org/apache/jena/mem/graph/TestGraphDelete.java |   17 +-
 .../TestGraphFindAllWithForEachRemaining.java      |   36 +-
 .../graph/TestGraphFindAllWithHasNextAndNext.java  |   39 +-
 .../mem/graph/TestGraphFindByMatchAndCount.java    |   85 +-
 .../mem/graph/TestGraphFindByMatchAndGetFirst.java |   61 +-
 .../jena/mem/graph/TestGraphMemoryConsumption.java |   38 +-
 .../apache/jena/mem/graph/TestGraphStreamAll.java  |   47 +-
 .../mem/graph/TestGraphStreamByMatchAndCount.java  |   48 +-
 .../graph/TestGraphStreamByMatchAndFindAny.java    |   59 +-
 .../org/apache/jena/mem/graph/helper/Context.java  |   41 +-
 .../mem/graph/helper/GraphTripleNodeHelper480.java |   15 +-
 .../graph/helper/GraphTripleNodeHelperCurrent.java |   15 +-
 .../jena/mem/graph/helper/JMHDefaultOptions.java   |    2 +-
 .../org/apache/jena/mem/graph/helper/Releases.java |    4 +-
 .../graph/helper => mem2}/JMHDefaultOptions.java   |    4 +-
 .../map/node/FastHashNodeMap.java}                 |   24 +-
 .../jena/mem2/map/node/HashCommonNodeMap.java      |   59 ++
 .../org/apache/jena/mem2/map/node/TestMapAdd.java  |  112 +++
 .../apache/jena/mem2/map/node/TestMapContains.java |  218 +++++
 .../set/triple/FastHashTripleSet.java}             |   24 +-
 .../set/triple/HashCommonTripleSet.java}           |   27 +-
 .../set/triple/TestSetAdd.java}                    |   73 +-
 .../jena/mem2/set/triple/TestSetContains.java      |  119 +++
 .../jena/mem2/set/triple/TestSetIterate.java       |  125 +++
 .../set/triple/TestSetMemoryConsumption.java}      |  104 +-
 .../apache/jena/mem2/set/triple/TestSetRemove.java |  124 +++
 .../jena/mem2/set/triple/TestSetStreamAll.java     |  120 +++
 .../apache/jena/mem2/set/triple/TestSetUpdate.java |  143 +++
 ...estSparseArraySpliteratorsForeachRemaining.java |   39 +-
 .../TestSparseArraySpliteratorsStreamParallel.java |   38 +-
 .../TestSparseArraySpliteratorsTryAdvance.java     |   47 +-
 jena-core/pom.xml                                  |   39 +-
 .../main/java/org/apache/jena/mem/GraphMem.java    |   23 +-
 .../java/org/apache/jena/mem/GraphMemBase.java     |   18 +-
 .../main/java/org/apache/jena/mem2/GraphMem2.java  |  169 ++++
 .../java/org/apache/jena/mem2/GraphMem2Fast.java   |   45 +
 .../java/org/apache/jena/mem2/GraphMem2Legacy.java |   47 +
 .../org/apache/jena/mem2/GraphMem2Roaring.java     |   49 +
 .../apache/jena/mem2/collection/FastHashBase.java  |  422 ++++++++
 .../apache/jena/mem2/collection/FastHashMap.java   |  208 ++++
 .../apache/jena/mem2/collection/FastHashSet.java   |  113 +++
 .../jena/mem2/collection/HashCommonBase.java       |  210 ++++
 .../apache/jena/mem2/collection/HashCommonMap.java |  193 ++++
 .../apache/jena/mem2/collection/HashCommonSet.java |   95 ++
 .../org/apache/jena/mem2/collection/JenaMap.java   |  121 +++
 .../jena/mem2/collection/JenaMapSetCommon.java     |  113 +++
 .../org/apache/jena/mem2/collection/JenaSet.java   |   28 +-
 .../jena/mem2/collection/JenaSetHashOptimized.java |   19 +-
 .../jena/mem2/iterator/IteratorOfJenaSets.java     |   74 ++
 .../jena/mem2/iterator/SparseArrayIterator.java    |   97 ++
 .../org/apache/jena/mem2/pattern/MatchPattern.java |   58 ++
 .../jena/mem2/pattern/PatternClassifier.java       |  105 ++
 .../jena/mem2/spliterator/ArraySpliterator.java    |  101 ++
 .../jena/mem2/spliterator/ArraySubSpliterator.java |  105 ++
 .../mem2/spliterator/SparseArraySpliterator.java   |  106 +++
 .../spliterator/SparseArraySubSpliterator.java     |  113 +++
 .../org/apache/jena/mem2/store/TripleStore.java    |   90 ++
 .../jena/mem2/store/fast/FastArrayBunch.java       |  192 ++++
 .../jena/mem2/store/fast/FastHashedBunchMap.java   |   23 +-
 .../mem2/store/fast/FastHashedTripleBunch.java     |   51 +
 .../jena/mem2/store/fast/FastTripleBunch.java      |   51 +
 .../jena/mem2/store/fast/FastTripleStore.java      |  386 ++++++++
 .../apache/jena/mem2/store/legacy/ArrayBunch.java  |  166 ++++
 .../apache/jena/mem2/store/legacy/FieldFilter.java |   69 ++
 .../jena/mem2/store/legacy/HashedBunchMap.java     |   28 +-
 .../jena/mem2/store/legacy/HashedTripleBunch.java  |   33 +-
 .../jena/mem2/store/legacy/LegacyTripleStore.java  |  152 +++
 .../jena/mem2/store/legacy/NodeToTriplesMap.java   |   61 ++
 .../mem2/store/legacy/NodeToTriplesMapMem.java     |  201 ++++
 .../apache/jena/mem2/store/legacy/TripleBunch.java |   23 +-
 .../store/roaring/RoaringBitmapTripleIterator.java |   94 ++
 .../mem2/store/roaring/RoaringTripleStore.java     |  335 +++++++
 .../java/org/apache/jena/graph/test/TestGraph.java |   35 +-
 .../apache/jena/mem2/AbstractGraphMem2Test.java    | 1003 ++++++++++++++++++++
 .../org/apache/jena/mem2/GraphMem2FastTest.java    |   19 +-
 .../org/apache/jena/mem2/GraphMem2LegacyTest.java  |   18 +-
 .../org/apache/jena/mem2/GraphMem2RoaringTest.java |   18 +-
 .../java/org/apache/jena/mem2/GraphMem2Test.java   |  173 ++++
 .../mem2/collection/AbstractJenaMapNodeTest.java   |  648 +++++++++++++
 .../mem2/collection/AbstractJenaSetTripleTest.java |  388 ++++++++
 .../jena/mem2/collection/FastHashMapTest.java      |   26 +-
 .../jena/mem2/collection/FastHashMapTest2.java     |   73 ++
 .../jena/mem2/collection/FastHashSetTest.java      |   23 +-
 .../jena/mem2/collection/FastHashSetTest2.java     |  168 ++++
 .../jena/mem2/collection/HashCommonMapTest.java    |   31 +-
 .../jena/mem2/collection/HashCommonSetTest.java    |   27 +-
 .../jena/mem2/iterator/IteratorOfJenaSetsTest.java |  107 +++
 .../mem2/iterator/SparseArrayIteratorTest.java     |  132 +++
 .../jena/mem2/pattern/PatternClassifierTest.java   |   52 +
 .../mem2/spliterator/ArraySpliteratorTest.java     |  295 ++++++
 .../mem2/spliterator/ArraySubSpliteratorTest.java  |  295 ++++++
 .../spliterator/SparseArraySpliteratorTest.java    |  554 +++++++++++
 .../spliterator/SparseArraySubSpliteratorTest.java |  554 +++++++++++
 .../jena/mem2/store/AbstractTripleStoreTest.java   | 1002 +++++++++++++++++++
 .../jena/mem2/store/fast/FastArrayBunchTest.java   |   24 +-
 .../mem2/store/fast/FastHashedTripleBunchTest.java |   63 ++
 .../jena/mem2/store/fast/FastTripleStoreTest.java  |  100 ++
 .../jena/mem2/store/legacy/ArrayBunchTest.java     |   20 +-
 .../jena/mem2/store/legacy/FieldFilterTest.java    |   69 ++
 .../mem2/store/legacy/HashedTripleBunchTest.java   |   51 +
 .../mem2/store/legacy/LegacyTripleStoreTest.java   |   21 +-
 .../mem2/store/legacy/NodeToTriplesMapMemTest.java |   20 +-
 .../roaring/RoaringBitmapTripleIteratorTest.java   |  144 +++
 .../mem2/store/roaring/RoaringTripleStoreTest.java |   21 +-
 pom.xml                                            |    9 +-
 108 files changed, 12148 insertions(+), 720 deletions(-)

diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphAdd.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphAdd.java
index e18f6f7747..1294780490 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphAdd.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphAdd.java
@@ -42,17 +42,17 @@ public class TestGraphAdd {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
-
+    java.util.function.Supplier<Object> graphAdd;
     private Context trialContext;
-
     private List<Triple> triplesCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triples480;
 
-    java.util.function.Supplier<Object> graphAdd;
-
     @Benchmark
     public Object graphAdd() {
         return graphAdd.get();
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsAnything.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsAnything.java
index 496fb237a3..1e70deb6c6 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsAnything.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsAnything.java
@@ -28,12 +28,15 @@ import org.junit.Test;
 import org.openjdk.jmh.annotations.*;
 import org.openjdk.jmh.runner.Runner;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Random;
 import java.util.function.Predicate;
 
 @State(Scope.Benchmark)
 public class TestGraphContainsAnything {
 
+
     @Param({
             "../testing/cheeses-0.1.ttl",
             "../testing/pizza.owl.rdf",
@@ -43,18 +46,18 @@ public class TestGraphContainsAnything {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
-
+    java.util.function.Function<String, Boolean> graphContains;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
-
     private List<Triple> triplesToFindCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triplesToFind480;
 
-    java.util.function.Function<String, Boolean> graphContains;
-
     @Benchmark
     public boolean graphContainsS__() {
         return graphContains.apply("S__");
@@ -65,6 +68,11 @@ public class TestGraphContainsAnything {
         return graphContains.apply("_P_");
     }
 
+    @Benchmark
+    public boolean graphContains__O() {
+        return graphContains.apply("__O");
+    }
+
     @Benchmark
     public boolean graphContainsSP_() {
         return graphContains.apply("SP_");
@@ -80,15 +88,12 @@ public class TestGraphContainsAnything {
         return graphContains.apply("_PO");
     }
 
-    @Benchmark
-    public boolean graphContains__O() {
-        return graphContains.apply("__O");
-    }
+
 
     private boolean graphContainsCurrent(String pattern) {
         var containsPredicate = getContainsPredicateByPatternCurrent(pattern);
         var found = false;
-        for(var t: triplesToFindCurrent) {
+        for (var t : triplesToFindCurrent) {
             found = containsPredicate.test(t);
             Assert.assertTrue(found);
         }
@@ -98,7 +103,7 @@ public class TestGraphContainsAnything {
     private boolean graphContains480(String pattern) {
         var containsPredicate = getContainsPredicateByPattern480(pattern);
         var found = false;
-        for(var t: triplesToFind480) {
+        for (var t : triplesToFind480) {
             found = containsPredicate.test(t);
             Assert.assertTrue(found);
         }
@@ -147,30 +152,34 @@ public class TestGraphContainsAnything {
     public void setupTrial() throws Exception {
         var trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphContains = this::graphContainsCurrent;
-
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
-                }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphContains = this::graphContains480;
-
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFind480 = Releases.v480.cloneTriples(triples);
-                }
-                break;
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphContains = this::graphContainsCurrent;
+
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
+
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                Collections.shuffle(this.triplesToFindCurrent, new Random(4721));
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphContains = this::graphContains480;
+
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                this.triplesToFind480 = Releases.v480.cloneTriples(triples);
+                Collections.shuffle(this.triplesToFind480, new Random(4721));
+            }
+            break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
         }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsTriple.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsTriple.java
index b61cdfbb06..7ae86b1d50 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsTriple.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphContainsTriple.java
@@ -29,6 +29,7 @@ import org.openjdk.jmh.annotations.*;
 import org.openjdk.jmh.runner.Runner;
 
 import java.util.List;
+import java.util.Random;
 
 
 @State(Scope.Benchmark)
@@ -42,19 +43,19 @@ public class TestGraphContainsTriple {
     public String param0_GraphUri;
 
     @Param({
-            "GraphMem (Jena 4.8.0)",
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
+            "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
-
+    java.util.function.Supplier<Boolean> graphContains;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
-
     private List<Triple> triplesToFindCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triplesToFind480;
 
-    java.util.function.Supplier<Boolean> graphContains;
-
     @Benchmark
     public boolean graphContains() {
         return graphContains.get();
@@ -62,7 +63,7 @@ public class TestGraphContainsTriple {
 
     private boolean graphContainsCurrent() {
         var found = false;
-        for(var t: triplesToFindCurrent) {
+        for (var t : triplesToFindCurrent) {
             found = sutCurrent.contains(t);
             Assert.assertTrue(found);
         }
@@ -71,7 +72,7 @@ public class TestGraphContainsTriple {
 
     private boolean graphContains480() {
         var found = false;
-        for(var t: triplesToFind480) {
+        for (var t : triplesToFind480) {
             found = sut480.contains(t);
             Assert.assertTrue(found);
         }
@@ -83,30 +84,34 @@ public class TestGraphContainsTriple {
     public void setupTrial() throws Exception {
         var trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphContains = this::graphContainsCurrent;
-
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
-                }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphContains = this::graphContains480;
-
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFind480 = Releases.v480.cloneTriples(triples);
-                }
-                break;
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphContains = this::graphContainsCurrent;
+
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFindCurrent, new Random(4721));
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphContains = this::graphContains480;
+
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFind480 = Releases.v480.cloneTriples(triples);
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFind480, new Random(4721));
+            }
+            break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
         }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphDelete.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphDelete.java
index 11ec23820d..6580958858 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphDelete.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphDelete.java
@@ -29,6 +29,7 @@ import org.openjdk.jmh.annotations.*;
 import org.openjdk.jmh.runner.Runner;
 
 import java.util.List;
+import java.util.Random;
 
 @State(Scope.Benchmark)
 public class TestGraphDelete {
@@ -42,23 +43,21 @@ public class TestGraphDelete {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
+    java.util.function.Supplier<Integer> graphDelete;
     private Context trialContext;
-
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
-
     private List<Triple> allTriplesCurrent;
     private List<org.apache.shadedJena480.graph.Triple> allTriples480;
-
     private List<Triple> triplesToDeleteFromSutCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triplesToDeleteFromSut480;
 
-
-    java.util.function.Supplier<Integer> graphDelete;
-
     @Benchmark
     public int graphDelete() {
         return graphDelete.get();
@@ -84,6 +83,9 @@ public class TestGraphDelete {
                 this.allTriplesCurrent.forEach(this.sutCurrent::add);
                 /*cloning is important so that the triples are not reference equal */
                 this.triplesToDeleteFromSutCurrent = Releases.current.cloneTriples(this.allTriplesCurrent);
+                /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToDeleteFromSutCurrent, new Random(4721));
                 break;
 
             case JENA_4_8_0:
@@ -91,6 +93,9 @@ public class TestGraphDelete {
                 this.allTriples480.forEach(this.sut480::add);
                 /*cloning is important so that the triples are not reference equal */
                 this.triplesToDeleteFromSut480 = Releases.v480.cloneTriples(this.allTriples480);
+                /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToDeleteFromSut480, new Random(4721));
                 break;
 
             default:
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithForEachRemaining.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithForEachRemaining.java
index bfddc7618d..b8a6639dc9 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithForEachRemaining.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithForEachRemaining.java
@@ -43,14 +43,16 @@ public class TestGraphFindAllWithForEachRemaining {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
+    java.util.function.Supplier<Long> graphFindAll;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
 
-    java.util.function.Supplier<Long> graphFindAll;
-
     @Benchmark
     public Long graphFindAll() {
         return graphFindAll.get();
@@ -78,24 +80,22 @@ public class TestGraphFindAllWithForEachRemaining {
     public void setupTrial() throws Exception {
         Context trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphFindAll = this::graphFindAllCurrent;
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphFindAll = this::graphFindAllCurrent;
 
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-                }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphFindAll = this::graphFindAll480;
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphFindAll = this::graphFindAll480;
 
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-                }
-                break;
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+            }
+            break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
         }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithHasNextAndNext.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithHasNextAndNext.java
index a4e91236d8..dc38c52eff 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithHasNextAndNext.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindAllWithHasNextAndNext.java
@@ -43,15 +43,16 @@ public class TestGraphFindAllWithHasNextAndNext {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
-
+    java.util.function.Supplier<Long> graphFindAll;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
 
-    java.util.function.Supplier<Long> graphFindAll;
-
     @Benchmark
     public Long graphFindAll() {
         return graphFindAll.get();
@@ -60,7 +61,7 @@ public class TestGraphFindAllWithHasNextAndNext {
     private Long graphFindAllCurrent() {
         var actionCounter = new ActionCount<>();
         var iter = sutCurrent.find();
-        while(iter.hasNext()) {
+        while (iter.hasNext()) {
             actionCounter.accept(iter.next());
         }
         iter.close();
@@ -71,7 +72,7 @@ public class TestGraphFindAllWithHasNextAndNext {
     private Long graphFindAll480() {
         var actionCounter = new ActionCount<>();
         var iter = sut480.find();
-        while(iter.hasNext()) {
+        while (iter.hasNext()) {
             actionCounter.accept(iter.next());
         }
         iter.close();
@@ -83,23 +84,21 @@ public class TestGraphFindAllWithHasNextAndNext {
     public void setupTrial() throws Exception {
         Context trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphFindAll = this::graphFindAllCurrent;
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphFindAll = this::graphFindAllCurrent;
 
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-                }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphFindAll = this::graphFindAll480;
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphFindAll = this::graphFindAll480;
 
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-                }
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+            }
             break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndCount.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndCount.java
index 156a7a8413..5a879e075d 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndCount.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndCount.java
@@ -31,6 +31,7 @@ import org.openjdk.jmh.runner.Runner;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Random;
 import java.util.function.Function;
 
 import static org.junit.Assert.assertNotNull;
@@ -48,21 +49,21 @@ public class TestGraphFindByMatchAndCount {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
 
-    @Param({"500"})
+    @Param({"800"})
     public int param2_sampleSize;
-
+    Function<String, Object> graphFindByMatchesAndCount;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
-
     private List<Triple> triplesToFindCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triplesToFind480;
 
-    Function<String, Object> graphFindByMatchesAndCount;
-
     @Benchmark
     public Object graphFindS__() {
         return graphFindByMatchesAndCount.apply("S__");
@@ -73,6 +74,11 @@ public class TestGraphFindByMatchAndCount {
         return graphFindByMatchesAndCount.apply("_P_");
     }
 
+    @Benchmark
+    public Object graphFind__O() {
+        return graphFindByMatchesAndCount.apply("__O");
+    }
+
     @Benchmark
     public Object graphFindSP_() {
         return graphFindByMatchesAndCount.apply("SP_");
@@ -88,10 +94,6 @@ public class TestGraphFindByMatchAndCount {
         return graphFindByMatchesAndCount.apply("_PO");
     }
 
-    @Benchmark
-    public Object graphFind__O() {
-        return graphFindByMatchesAndCount.apply("__O");
-    }
 
     private int graphFindByMatchesAndCount(String pattern) {
         var findFunction = getFindFunctionByPatternCurrent(pattern);
@@ -153,39 +155,42 @@ public class TestGraphFindByMatchAndCount {
     public void setupTrial() throws Exception {
         Context trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphFindByMatchesAndCount = this::graphFindByMatchesAndCount;
-
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFindCurrent = new ArrayList<>(param2_sampleSize);
-                    var sampleIncrement = triples.size() / param2_sampleSize;
-                    for(var i=0; i< triples.size(); i+=sampleIncrement) {
-                        this.triplesToFindCurrent.add(Releases.current.cloneTriple(triples.get(i)));
-                    }
-
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphFindByMatchesAndCount = this::graphFindByMatchesAndCount;
+
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFindCurrent = new ArrayList<>(param2_sampleSize);
+                var sampleIncrement = triples.size() / param2_sampleSize;
+                for (var i = 0; i < triples.size(); i += sampleIncrement) {
+                    this.triplesToFindCurrent.add(Releases.current.cloneTriple(triples.get(i)));
                 }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphFindByMatchesAndCount = this::graphFindByMatchesAndCount480;
-
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFind480 = new ArrayList<>(param2_sampleSize);
-                    var sampleIncrement = triples.size() / param2_sampleSize;
-                    for(var i=0; i< triples.size(); i+=sampleIncrement) {
-                        this.triplesToFind480.add(Releases.v480.cloneTriple(triples.get(i)));
-                    }
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFindCurrent, new Random(4721));
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphFindByMatchesAndCount = this::graphFindByMatchesAndCount480;
+
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFind480 = new ArrayList<>(param2_sampleSize);
+                var sampleIncrement = triples.size() / param2_sampleSize;
+                for (var i = 0; i < triples.size(); i += sampleIncrement) {
+                    this.triplesToFind480.add(Releases.v480.cloneTriple(triples.get(i)));
                 }
-                break;
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFind480, new Random(4721));
+            }
+            break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
         }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndGetFirst.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndGetFirst.java
index b440a389af..6b398e4cd8 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndGetFirst.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphFindByMatchAndGetFirst.java
@@ -29,6 +29,7 @@ import org.openjdk.jmh.runner.Runner;
 
 import java.util.Iterator;
 import java.util.List;
+import java.util.Random;
 import java.util.function.Function;
 
 import static org.junit.Assert.assertNotNull;
@@ -47,18 +48,18 @@ public class TestGraphFindByMatchAndGetFirst {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
-
+    java.util.function.Function<String, Object> graphFind;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
-
     private List<Triple> triplesToFindCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triplesToFind480;
 
-    java.util.function.Function<String, Object> graphFind;
-
     @Benchmark
     public Object graphFindS__() {
         return graphFind.apply("S__");
@@ -155,30 +156,34 @@ public class TestGraphFindByMatchAndGetFirst {
     public void setupTrial() throws Exception {
         Context trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphFind = this::graphFindByMatchesAndGetFirstCurrent;
-
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
-                }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphFind = this::graphFindByMatchesAndGetFirst480;
-
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFind480 = Releases.v480.cloneTriples(triples);
-                }
-                break;
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphFind = this::graphFindByMatchesAndGetFirstCurrent;
+
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFindCurrent, new Random(4721));
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphFind = this::graphFindByMatchesAndGetFirst480;
+
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFind480 = Releases.v480.cloneTriples(triples);
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFind480, new Random(4721));
+            }
+            break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
         }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphMemoryConsumption.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphMemoryConsumption.java
index dd59e405f4..ca56cce49c 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphMemoryConsumption.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphMemoryConsumption.java
@@ -43,15 +43,29 @@ public class TestGraphMemoryConsumption {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
+    java.util.function.Supplier<Object> graphFill;
     private Context trialContext;
-
     private List<Triple> allTriplesCurrent;
     private List<org.apache.shadedJena480.graph.Triple> allTriples480;
 
-    java.util.function.Supplier<Object> graphFill;
+    /**
+     * This method is used to get the memory consumption of the current JVM.
+     *
+     * @return the memory consumption in MB
+     */
+    private static double runGcAndGetUsedMemoryInMB() {
+        System.runFinalization();
+        System.gc();
+        Runtime.getRuntime().runFinalization();
+        Runtime.getRuntime().gc();
+        return BigDecimal.valueOf(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()).divide(BigDecimal.valueOf(1024L)).divide(BigDecimal.valueOf(1024L)).doubleValue();
+    }
 
     @Benchmark
     public Object graphFill() {
@@ -65,10 +79,10 @@ public class TestGraphMemoryConsumption {
         allTriplesCurrent.forEach(sut::add);
         stopwatch.stop();
         var memoryAfter = runGcAndGetUsedMemoryInMB();
-        System.out.println(String.format("graphs: %d time to fill graphs: %s additional memory: %5.3f MB",
+        System.out.printf("graphs: %d time to fill graphs: %s additional memory: %5.3f MB%n",
                 sut.size(),
                 stopwatch.formatTime(),
-                (memoryAfter - memoryBefore)));
+                (memoryAfter - memoryBefore));
         return sut;
     }
 
@@ -79,25 +93,13 @@ public class TestGraphMemoryConsumption {
         allTriples480.forEach(sut::add);
         stopwatch.stop();
         var memoryAfter = runGcAndGetUsedMemoryInMB();
-        System.out.println(String.format("graphs: %d time to fill graphs: %s additional memory: %5.3f MB",
+        System.out.printf("graphs: %d time to fill graphs: %s additional memory: %5.3f MB%n",
                 sut.size(),
                 stopwatch.formatTime(),
-                (memoryAfter - memoryBefore)));
+                (memoryAfter - memoryBefore));
         return sut;
     }
 
-    /**
-     * This method is used to get the memory consumption of the current JVM.
-     * @return the memory consumption in MB
-     */
-    private static double runGcAndGetUsedMemoryInMB() {
-        System.runFinalization();
-        System.gc();
-        Runtime.getRuntime().runFinalization();
-        Runtime.getRuntime().gc();
-        return BigDecimal.valueOf(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()).divide(BigDecimal.valueOf(1024l)).divide(BigDecimal.valueOf(1024l)).doubleValue();
-    }
-
     @Setup(Level.Trial)
     public void setupTrial() throws Exception {
         this.trialContext = new Context(param1_GraphImplementation);
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamAll.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamAll.java
index 82e3d3847d..4cb1960daf 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamAll.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamAll.java
@@ -44,15 +44,16 @@ public class TestGraphStreamAll {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
-
-    private Graph sutCurrent;
-    private org.apache.shadedJena480.graph.Graph sut480;
-
     java.util.function.Supplier<Object> graphStream;
     java.util.function.Supplier<Object> graphStreamParallel;
+    private Graph sutCurrent;
+    private org.apache.shadedJena480.graph.Graph sut480;
 
     @Benchmark
     public Object graphStream() {
@@ -92,26 +93,24 @@ public class TestGraphStreamAll {
     public void setupTrial() throws Exception {
         Context trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphStream = this::graphStreamCurrent;
-                    this.graphStreamParallel = this::graphStreamParallelCurrent;
-
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-                }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphStream = this::graphStream480;
-                    this.graphStreamParallel = this::graphStreamParallel480;
-
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-                }
-                break;
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphStream = this::graphStreamCurrent;
+                this.graphStreamParallel = this::graphStreamParallelCurrent;
+
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphStream = this::graphStream480;
+                this.graphStreamParallel = this::graphStreamParallel480;
+
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+            }
+            break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
         }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndCount.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndCount.java
index 06707157d9..1985462a9c 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndCount.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndCount.java
@@ -30,6 +30,7 @@ import org.openjdk.jmh.runner.Runner;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Random;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
@@ -48,20 +49,26 @@ public class TestGraphStreamByMatchAndCount {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
 
-    @Param({"500"})
+    @Param({"800"})
     public int param2_sampleSize;
-
+    Function<String, Object> graphStreamByMatchAndCount;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
-
     private List<Triple> triplesToFindCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triplesToFind480;
 
-    Function<String, Object> graphStreamByMatchAndCount;
+    private static int count(final Stream stream) {
+        var actionCounter = new ActionCount<>();
+        stream.forEach(actionCounter::accept);
+        return (int) actionCounter.getCount();
+    }
 
     @Benchmark
     public Object graphStreamS__() {
@@ -73,6 +80,11 @@ public class TestGraphStreamByMatchAndCount {
         return graphStreamByMatchAndCount.apply("_P_");
     }
 
+    @Benchmark
+    public Object graphStream__O() {
+        return graphStreamByMatchAndCount.apply("__O");
+    }
+
     @Benchmark
     public Object graphStreamSP_() {
         return graphStreamByMatchAndCount.apply("SP_");
@@ -88,11 +100,6 @@ public class TestGraphStreamByMatchAndCount {
         return graphStreamByMatchAndCount.apply("_PO");
     }
 
-    @Benchmark
-    public Object graphStream__O() {
-        return graphStreamByMatchAndCount.apply("__O");
-    }
-
     private int graphStreamByMatchAndCount(String pattern) {
         var streamFunction = getStreamFunctionByPatternCurrent(pattern);
         var total = 0;
@@ -111,12 +118,6 @@ public class TestGraphStreamByMatchAndCount {
         return total;
     }
 
-    private static int count(final Stream stream) {
-        var actionCounter = new ActionCount<>();
-        stream.forEach(actionCounter::accept);
-        return (int)actionCounter.getCount();
-    }
-
     Function<Triple, Stream<Triple>> getStreamFunctionByPatternCurrent(String pattern) {
         switch (pattern) {
             case "S__":
@@ -159,8 +160,7 @@ public class TestGraphStreamByMatchAndCount {
     public void setupTrial() throws Exception {
         Context trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-            {
+            case CURRENT: {
                 this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
                 this.graphStreamByMatchAndCount = this::graphStreamByMatchAndCount;
 
@@ -170,14 +170,15 @@ public class TestGraphStreamByMatchAndCount {
                 /*clone the triples because they should not be the same objects*/
                 this.triplesToFindCurrent = new ArrayList<>(param2_sampleSize);
                 var sampleIncrement = triples.size() / param2_sampleSize;
-                for(var i=0; i< triples.size(); i+=sampleIncrement) {
+                for (var i = 0; i < triples.size(); i += sampleIncrement) {
                     this.triplesToFindCurrent.add(Releases.current.cloneTriple(triples.get(i)));
                 }
-
+                /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFindCurrent, new Random(4721));
             }
             break;
-            case JENA_4_8_0:
-            {
+            case JENA_4_8_0: {
                 this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
                 this.graphStreamByMatchAndCount = this::graphStreamByMatchAndCount480;
 
@@ -187,9 +188,12 @@ public class TestGraphStreamByMatchAndCount {
                 /*clone the triples because they should not be the same objects*/
                 this.triplesToFind480 = new ArrayList<>(param2_sampleSize);
                 var sampleIncrement = triples.size() / param2_sampleSize;
-                for(var i=0; i< triples.size(); i+=sampleIncrement) {
+                for (var i = 0; i < triples.size(); i += sampleIncrement) {
                     this.triplesToFind480.add(Releases.v480.cloneTriple(triples.get(i)));
                 }
+                /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFind480, new Random(4721));
             }
             break;
             default:
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndFindAny.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndFindAny.java
index 2b7c776140..a97de8aab8 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndFindAny.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphStreamByMatchAndFindAny.java
@@ -29,6 +29,7 @@ import org.openjdk.jmh.runner.Runner;
 
 import java.util.List;
 import java.util.Optional;
+import java.util.Random;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
@@ -48,18 +49,18 @@ public class TestGraphStreamByMatchAndFindAny {
 
     @Param({
             "GraphMem (current)",
+            "GraphMem2Fast (current)",
+            "GraphMem2Legacy (current)",
+            "GraphMem2Roaring (current)",
             "GraphMem (Jena 4.8.0)",
     })
     public String param1_GraphImplementation;
-
+    java.util.function.Function<String, Object> graphStream;
     private Graph sutCurrent;
     private org.apache.shadedJena480.graph.Graph sut480;
-
     private List<Triple> triplesToFindCurrent;
     private List<org.apache.shadedJena480.graph.Triple> triplesToFind480;
 
-    java.util.function.Function<String, Object> graphStream;
-
     @Benchmark
     public Object graphStreamS__() {
         return graphStream.apply("S__");
@@ -156,29 +157,33 @@ public class TestGraphStreamByMatchAndFindAny {
     public void setupTrial() throws Exception {
         Context trialContext = new Context(param1_GraphImplementation);
         switch (trialContext.getJenaVersion()) {
-            case CURRENT:
-                {
-                    this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-                    this.graphStream = this::graphStreamByMatchAndFindAnyCurrent;
-
-                    var triples = Releases.current.readTriples(param0_GraphUri);
-                    triples.forEach(this.sutCurrent::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
-                }
-                break;
-            case JENA_4_8_0:
-                {
-                    this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-                    this.graphStream = this::graphFindByMatcheAndFindAny480;
-
-                    var triples = Releases.v480.readTriples(param0_GraphUri);
-                    triples.forEach(this.sut480::add);
-
-                    /*clone the triples because they should not be the same objects*/
-                    this.triplesToFind480 = Releases.v480.cloneTriples(triples);
-                }
+            case CURRENT: {
+                this.sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
+                this.graphStream = this::graphStreamByMatchAndFindAnyCurrent;
+
+                var triples = Releases.current.readTriples(param0_GraphUri);
+                triples.forEach(this.sutCurrent::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFindCurrent = Releases.current.cloneTriples(triples);
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFindCurrent, new Random(4721));
+            }
+            break;
+            case JENA_4_8_0: {
+                this.sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
+                this.graphStream = this::graphFindByMatcheAndFindAny480;
+
+                var triples = Releases.v480.readTriples(param0_GraphUri);
+                triples.forEach(this.sut480::add);
+
+                /*clone the triples because they should not be the same objects*/
+                this.triplesToFind480 = Releases.v480.cloneTriples(triples);
+                    /* Shuffle is import because the order might play a role. We want to test the performance of the
+                       contains method regardless of the order */
+                java.util.Collections.shuffle(this.triplesToFind480, new Random(4721));
+            }
             break;
             default:
                 throw new IllegalArgumentException("Unknown Jena version: " + trialContext.getJenaVersion());
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Context.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Context.java
index 7e288e7e32..43c6df65e2 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Context.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Context.java
@@ -24,28 +24,27 @@ import org.apache.jena.JenaVersion;
  */
 public class Context {
 
-    public GraphClass getGraphClass() {
-        return graphClass;
-    }
-
-    public JenaVersion getJenaVersion() {
-        return jenaVersion;
-    }
-
-    public enum GraphClass {
-        GraphMem
-    }
-
     private final GraphClass graphClass;
     private final JenaVersion jenaVersion;
 
-
     public Context(String graphImplementation) {
         switch (graphImplementation) {
             case "GraphMem (current)":
                 this.graphClass = GraphClass.GraphMem;
                 this.jenaVersion = JenaVersion.CURRENT;
                 break;
+            case "GraphMem2Fast (current)":
+                this.graphClass = GraphClass.GraphMem2Fast;
+                this.jenaVersion = JenaVersion.CURRENT;
+                break;
+            case "GraphMem2Legacy (current)":
+                this.graphClass = GraphClass.GraphMem2Legacy;
+                this.jenaVersion = JenaVersion.CURRENT;
+                break;
+            case "GraphMem2Roaring (current)":
+                this.graphClass = GraphClass.GraphMem2Roaring;
+                this.jenaVersion = JenaVersion.CURRENT;
+                break;
             case "GraphMem (Jena 4.8.0)":
                 this.graphClass = GraphClass.GraphMem;
                 this.jenaVersion = JenaVersion.JENA_4_8_0;
@@ -55,5 +54,21 @@ public class Context {
         }
     }
 
+    public GraphClass getGraphClass() {
+        return graphClass;
+    }
+
+    public JenaVersion getJenaVersion() {
+        return jenaVersion;
+    }
+
+
+    public enum GraphClass {
+        GraphMem,
+        GraphMem2Fast,
+        GraphMem2Legacy,
+        GraphMem2Roaring,
+    }
+
 
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelper480.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelper480.java
index 1e528b557e..b0162fc87a 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelper480.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelper480.java
@@ -26,17 +26,16 @@ import org.apache.shadedJena480.riot.RDFDataMgr;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 public class GraphTripleNodeHelper480 implements GraphTripleNodeHelper<Graph, Triple, Node> {
 
     @Override
     public Graph createGraph(Context.GraphClass graphClass) {
-        switch (graphClass) {
-            case GraphMem:
-                return new GraphMem();
-            default:
-                throw new IllegalArgumentException("Unknown graph class: " + graphClass);
+        if (Objects.requireNonNull(graphClass) == Context.GraphClass.GraphMem) {
+            return new GraphMem();
         }
+        throw new IllegalArgumentException("Unknown graph class: " + graphClass);
     }
 
     @Override
@@ -67,13 +66,13 @@ public class GraphTripleNodeHelper480 implements GraphTripleNodeHelper<Graph, Tr
 
     @Override
     public Node cloneNode(Node node) {
-        if(node.isLiteral()) {
+        if (node.isLiteral()) {
             return NodeFactory.createLiteralByValue(node.getLiteralLexicalForm(), node.getLiteralLanguage(), node.getLiteralDatatype());
         }
-        if(node.isURI()) {
+        if (node.isURI()) {
             return NodeFactory.createURI(node.getURI());
         }
-        if(node.isBlank()) {
+        if (node.isBlank()) {
             return NodeFactory.createBlankNode(node.getBlankNodeLabel());
         }
         throw new IllegalArgumentException("Only literals, URIs and blank nodes are supported");
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelperCurrent.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelperCurrent.java
index 88f5aa4d0e..532a583571 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelperCurrent.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/GraphTripleNodeHelperCurrent.java
@@ -22,6 +22,9 @@ import org.apache.jena.graph.Node;
 import org.apache.jena.graph.NodeFactory;
 import org.apache.jena.graph.Triple;
 import org.apache.jena.mem.GraphMem;
+import org.apache.jena.mem2.GraphMem2Fast;
+import org.apache.jena.mem2.GraphMem2Legacy;
+import org.apache.jena.mem2.GraphMem2Roaring;
 import org.apache.jena.riot.RDFDataMgr;
 
 import java.util.ArrayList;
@@ -34,6 +37,12 @@ public class GraphTripleNodeHelperCurrent implements GraphTripleNodeHelper<Graph
         switch (graphClass) {
             case GraphMem:
                 return new GraphMem();
+            case GraphMem2Fast:
+                return new GraphMem2Fast();
+            case GraphMem2Legacy:
+                return new GraphMem2Legacy();
+            case GraphMem2Roaring:
+                return new GraphMem2Roaring();
             default:
                 throw new IllegalArgumentException("Unknown graph class: " + graphClass);
         }
@@ -67,13 +76,13 @@ public class GraphTripleNodeHelperCurrent implements GraphTripleNodeHelper<Graph
 
     @Override
     public Node cloneNode(Node node) {
-        if(node.isLiteral()) {
+        if (node.isLiteral()) {
             return NodeFactory.createLiteralByValue(node.getLiteralLexicalForm(), node.getLiteralLanguage(), node.getLiteralDatatype());
         }
-        if(node.isURI()) {
+        if (node.isURI()) {
             return NodeFactory.createURI(node.getURI());
         }
-        if(node.isBlank()) {
+        if (node.isBlank()) {
             return NodeFactory.createBlankNode(node.getBlankNodeLabel());
         }
         throw new IllegalArgumentException("Only literals, URIs and blank nodes are supported");
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/JMHDefaultOptions.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/JMHDefaultOptions.java
index 1f88776775..60e41d4103 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/JMHDefaultOptions.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/JMHDefaultOptions.java
@@ -37,7 +37,7 @@ public class JMHDefaultOptions {
                 // You can be more specific if you'd like to run only one benchmark per test.
                 .include(c.getName())
                 // Set the following options as needed
-                .mode (Mode.AverageTime)
+                .mode(Mode.AverageTime)
                 .timeUnit(TimeUnit.SECONDS)
                 .warmupTime(TimeValue.NONE)
                 .warmupIterations(5)
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
index f601955053..a03623eae8 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
@@ -26,7 +26,7 @@ import org.apache.jena.graph.Triple;
  */
 public class Releases {
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
+    public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
 
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/JMHDefaultOptions.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/JMHDefaultOptions.java
similarity index 96%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/JMHDefaultOptions.java
copy to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/JMHDefaultOptions.java
index 1f88776775..adb152256b 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/JMHDefaultOptions.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/JMHDefaultOptions.java
@@ -15,7 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.helper;
 
 import org.openjdk.jmh.annotations.Mode;
 import org.openjdk.jmh.results.format.ResultFormatType;
@@ -37,7 +37,7 @@ public class JMHDefaultOptions {
                 // You can be more specific if you'd like to run only one benchmark per test.
                 .include(c.getName())
                 // Set the following options as needed
-                .mode (Mode.AverageTime)
+                .mode(Mode.AverageTime)
                 .timeUnit(TimeUnit.SECONDS)
                 .warmupTime(TimeValue.NONE)
                 .warmupIterations(5)
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/FastHashNodeMap.java
similarity index 60%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/FastHashNodeMap.java
index f601955053..4557ca07a5 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/FastHashNodeMap.java
@@ -15,18 +15,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.map.node;
 
-import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.FastHashMap;
 
-/**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
- */
-public class Releases {
+public class FastHashNodeMap extends FastHashMap<Node, Object> {
+
+    public FastHashNodeMap() {
+        super();
+    }
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    @Override
+    protected Object[] newValuesArray(int size) {
+        return new Object[size];
+    }
 
+    @Override
+    protected Node[] newKeysArray(int size) {
+        return new Node[size];
+    }
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/HashCommonNodeMap.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/HashCommonNodeMap.java
new file mode 100644
index 0000000000..2b6ec52abb
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/HashCommonNodeMap.java
@@ -0,0 +1,59 @@
+/*
+ * 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.jena.mem2.map.node;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.mem2.collection.HashCommonMap;
+
+public class HashCommonNodeMap extends HashCommonMap<Node, Object> {
+
+    /**
+     * Initialise this hashed thingy to have <code>initialCapacity</code> as its
+     * capacity and the corresponding threshold. All the key elements start out
+     * null.
+     *
+     * @param initialCapacity
+     */
+    public HashCommonNodeMap(int initialCapacity) {
+        super(initialCapacity);
+    }
+
+    /**
+     * Initialise this hashed thingy to have <code>10</code> as its
+     * capacity and the corresponding threshold. All the key elements start out
+     * null.
+     */
+    public HashCommonNodeMap() {
+        this(10);
+    }
+
+    @Override
+    protected Node[] newKeysArray(int size) {
+        return new Node[size];
+    }
+
+    @Override
+    public void clear() {
+        super.clear(10);
+    }
+
+    @Override
+    protected Object[] newValuesArray(int size) {
+        return new Object[size];
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/TestMapAdd.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/TestMapAdd.java
new file mode 100644
index 0000000000..d30b7871b8
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/TestMapAdd.java
@@ -0,0 +1,112 @@
+/*
+ * 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.jena.mem2.map.node;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.HashMap;
+import java.util.List;
+
+
+@State(Scope.Benchmark)
+public class TestMapAdd {
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+            "../testing/BSBM/bsbm-1m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "HashMap",
+            "HashCommonNodeSet",
+            "FastHashNodeSet"
+    })
+    public String param1_SetImplementation;
+    java.util.function.Function<Triple.Field, Object> addToSet;
+    private List<Triple> triples;
+
+    @Benchmark
+    public Object addSubjectsToSet() {
+        return addToSet.apply(Triple.Field.fieldSubject);
+    }
+
+    @Benchmark
+    public Object addPredicatesToSet() {
+        return addToSet.apply(Triple.Field.fieldPredicate);
+    }
+
+    @Benchmark
+    public Object addObjectsToSet() {
+        return addToSet.apply(Triple.Field.fieldObject);
+    }
+
+    private Object addToHashMap(Triple.Field field) {
+        var sut = new HashMap<Node, Object>();
+        triples.forEach(t -> sut.put(field.getField(t), null));
+        return sut;
+    }
+
+    private Object addToFastHashNodeMap(Triple.Field field) {
+        var sut = new FastHashNodeMap();
+        triples.forEach(t -> sut.tryPut(field.getField(t), null));
+        return sut;
+    }
+
+    private Object addToHashCommonNodeMap(Triple.Field field) {
+        var sut = new HashCommonNodeMap();
+        triples.forEach(t -> sut.tryPut(field.getField(t), null));
+        return sut;
+    }
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        triples = Releases.current.readTriples(param0_GraphUri);
+        switch (param1_SetImplementation) {
+            case "HashMap":
+                this.addToSet = this::addToHashMap;
+                break;
+            case "HashCommonNodeSet":
+                this.addToSet = this::addToHashCommonNodeMap;
+                break;
+            case "FastHashNodeSet":
+                this.addToSet = this::addToFastHashNodeMap;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/TestMapContains.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/TestMapContains.java
new file mode 100644
index 0000000000..87cc71f7d5
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/map/node/TestMapContains.java
@@ -0,0 +1,218 @@
+/*
+ * 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.jena.mem2.map.node;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.HashMap;
+import java.util.List;
+
+
+@State(Scope.Benchmark)
+public class TestMapContains {
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+            "../testing/BSBM/bsbm-1m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "HashSet",
+            "HashCommonNodeSet",
+            "FastHashNodeSet"
+    })
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Boolean> setContainsSubjects;
+    java.util.function.Supplier<Boolean> setContainsPredicates;
+    java.util.function.Supplier<Boolean> setContainsObjects;
+    private List<Triple> triplesToFind;
+    private HashMap<Node, Object> subjectHashMap;
+    private HashMap<Node, Object> predicateHashMap;
+    private HashMap<Node, Object> objectHashMap;
+    private HashCommonNodeMap subjectHashCommonNodeMap;
+    private HashCommonNodeMap predicateHashCommonNodeMap;
+    private HashCommonNodeMap objectHashCommonNodeMap;
+    private FastHashNodeMap subjectFastHashNodeSet;
+    private FastHashNodeMap predicateFastHashNodeSet;
+    private FastHashNodeMap objectFastHashNodeSet;
+
+
+    @Benchmark
+    public boolean setContainsSubjects() {
+        return setContainsSubjects.get();
+    }
+
+    @Benchmark
+    public boolean setContainsPredicates() {
+        return setContainsPredicates.get();
+    }
+
+    @Benchmark
+    public boolean setContainsObjects() {
+        return setContainsObjects.get();
+    }
+
+    private boolean hashMapContainsSubjects() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = subjectHashMap.containsKey(t.getSubject());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean hashMapContainsPredicates() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = predicateHashMap.containsKey(t.getPredicate());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean hashMapContainsObjects() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = objectHashMap.containsKey(t.getObject());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+
+    private boolean HashCommonNodeMapContainsSubjects() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = subjectHashCommonNodeMap.containsKey(t.getSubject());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean HashCommonNodeMapContainsPredicates() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = predicateHashCommonNodeMap.containsKey(t.getPredicate());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean HashCommonNodeMapContainsObjects() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = objectHashCommonNodeMap.containsKey(t.getObject());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean FastHashNodeMapContainsSubjects() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = subjectFastHashNodeSet.containsKey(t.getSubject());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean FastHashNodeMapContainsPredicates() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = predicateFastHashNodeSet.containsKey(t.getPredicate());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean FastHashNodeMapContainsObjects() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = objectFastHashNodeSet.containsKey(t.getObject());
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        var triples = Releases.current.readTriples(param0_GraphUri);
+        this.triplesToFind = Releases.current.cloneTriples(triples);
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.subjectHashMap = new HashMap<>();
+                this.predicateHashMap = new HashMap<>();
+                this.objectHashMap = new HashMap<>();
+                triples.forEach(t -> {
+                    subjectHashMap.put(t.getSubject(), null);
+                    predicateHashMap.put(t.getPredicate(), null);
+                    objectHashMap.put(t.getObject(), null);
+                });
+                this.setContainsSubjects = this::hashMapContainsSubjects;
+                this.setContainsPredicates = this::hashMapContainsPredicates;
+                this.setContainsObjects = this::hashMapContainsObjects;
+                break;
+            case "HashCommonNodeSet":
+                this.subjectHashCommonNodeMap = new HashCommonNodeMap();
+                this.predicateHashCommonNodeMap = new HashCommonNodeMap();
+                this.objectHashCommonNodeMap = new HashCommonNodeMap();
+                triples.forEach(t -> {
+                    subjectHashCommonNodeMap.tryPut(t.getSubject(), null);
+                    predicateHashCommonNodeMap.tryPut(t.getPredicate(), null);
+                    objectHashCommonNodeMap.tryPut(t.getObject(), null);
+                });
+                this.setContainsSubjects = this::HashCommonNodeMapContainsSubjects;
+                this.setContainsPredicates = this::HashCommonNodeMapContainsPredicates;
+                this.setContainsObjects = this::HashCommonNodeMapContainsObjects;
+                break;
+            case "FastHashNodeSet":
+                this.subjectFastHashNodeSet = new FastHashNodeMap();
+                this.predicateFastHashNodeSet = new FastHashNodeMap();
+                this.objectFastHashNodeSet = new FastHashNodeMap();
+                triples.forEach(t -> {
+                    subjectFastHashNodeSet.tryPut(t.getSubject(), null);
+                    predicateFastHashNodeSet.tryPut(t.getPredicate(), null);
+                    objectFastHashNodeSet.tryPut(t.getObject(), null);
+                });
+                this.setContainsSubjects = this::FastHashNodeMapContainsSubjects;
+                this.setContainsPredicates = this::FastHashNodeMapContainsPredicates;
+                this.setContainsObjects = this::FastHashNodeMapContainsObjects;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/FastHashTripleSet.java
similarity index 60%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/FastHashTripleSet.java
index f601955053..991a7bcc80 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/FastHashTripleSet.java
@@ -15,18 +15,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.set.triple;
 
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.FastHashSet;
 
-/**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
- */
-public class Releases {
+public class FastHashTripleSet extends FastHashSet<Triple> {
+
+    public FastHashTripleSet() {
+        super();
+    }
+
+    public FastHashTripleSet(int initialCapacity) {
+        super(initialCapacity);
+    }
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
 
+    @Override
+    protected Triple[] newKeysArray(int size) {
+        return new Triple[size];
+    }
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/HashCommonTripleSet.java
similarity index 60%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/HashCommonTripleSet.java
index f601955053..98eab2aa0b 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/HashCommonTripleSet.java
@@ -15,18 +15,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.set.triple;
 
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.HashCommonSet;
 
-/**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
- */
-public class Releases {
+public class HashCommonTripleSet extends HashCommonSet<Triple> {
+    public HashCommonTripleSet() {
+        super(10);
+    }
+
+    public HashCommonTripleSet(int initialCapacity) {
+        super(initialCapacity);
+    }
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    @Override
+    public void clear() {
+        super.clear(10);
+    }
 
+    @Override
+    protected Triple[] newKeysArray(int size) {
+        return new Triple[size];
+    }
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphAdd.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetAdd.java
similarity index 51%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphAdd.java
copy to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetAdd.java
index e18f6f7747..c7990e1d1b 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphAdd.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetAdd.java
@@ -16,22 +16,22 @@
  * limitations under the License.
  */
 
-package org.apache.jena.mem.graph;
+package org.apache.jena.mem2.set.triple;
 
 import org.apache.jena.graph.Triple;
-import org.apache.jena.mem.graph.helper.Context;
-import org.apache.jena.mem.graph.helper.JMHDefaultOptions;
 import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
 import org.junit.Assert;
 import org.junit.Test;
 import org.openjdk.jmh.annotations.*;
 import org.openjdk.jmh.runner.Runner;
 
+import java.util.HashSet;
 import java.util.List;
 
 
 @State(Scope.Benchmark)
-public class TestGraphAdd {
+public class TestSetAdd {
 
     @Param({
             "../testing/cheeses-0.1.ttl",
@@ -41,52 +41,55 @@ public class TestGraphAdd {
     public String param0_GraphUri;
 
     @Param({
-            "GraphMem (current)",
-            "GraphMem (Jena 4.8.0)",
+            "HashSet",
+            "HashCommonTripleSet",
+            "FastHashTripleSet",
     })
-    public String param1_GraphImplementation;
-
-    private Context trialContext;
-
-    private List<Triple> triplesCurrent;
-    private List<org.apache.shadedJena480.graph.Triple> triples480;
-
-    java.util.function.Supplier<Object> graphAdd;
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Object> addToSet;
+    private List<Triple> triples;
 
     @Benchmark
-    public Object graphAdd() {
-        return graphAdd.get();
+    public Object addToSet() {
+        return addToSet.get();
     }
 
-    private Object graphAddCurrent() {
-        var sutCurrent = Releases.current.createGraph(trialContext.getGraphClass());
-        triplesCurrent.forEach(sutCurrent::add);
-        Assert.assertEquals(triplesCurrent.size(), sutCurrent.size());
-        return sutCurrent;
+    private Object addToHashSet() {
+        var sut = new HashSet<Triple>();
+        triples.forEach(sut::add);
+        Assert.assertEquals(triples.size(), sut.size());
+        return sut;
     }
 
-    private Object graphAdd480() {
-        var sut480 = Releases.v480.createGraph(trialContext.getGraphClass());
-        triples480.forEach(sut480::add);
-        Assert.assertEquals(triples480.size(), sut480.size());
-        return sut480;
+    private Object addToHashCommonTripleSet() {
+        var sut = new HashCommonTripleSet();
+        triples.forEach(sut::tryAdd);
+        Assert.assertEquals(triples.size(), sut.size());
+        return sut;
     }
 
+    private Object addToFastHashTripleSet() {
+        var sut = new FastHashTripleSet();
+        triples.forEach(sut::tryAdd);
+        Assert.assertEquals(triples.size(), sut.size());
+        return sut;
+    }
 
     @Setup(Level.Trial)
     public void setupTrial() throws Exception {
-        this.trialContext = new Context(param1_GraphImplementation);
-        switch (this.trialContext.getJenaVersion()) {
-            case CURRENT:
-                triplesCurrent = Releases.current.readTriples(param0_GraphUri);
-                this.graphAdd = this::graphAddCurrent;
+        triples = Releases.current.readTriples(param0_GraphUri);
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.addToSet = this::addToHashSet;
+                break;
+            case "HashCommonTripleSet":
+                this.addToSet = this::addToHashCommonTripleSet;
                 break;
-            case JENA_4_8_0:
-                triples480 = Releases.v480.readTriples(param0_GraphUri);
-                this.graphAdd = this::graphAdd480;
+            case "FastHashTripleSet":
+                this.addToSet = this::addToFastHashTripleSet;
                 break;
             default:
-                throw new IllegalArgumentException("Unknown Jena version: " + this.trialContext.getJenaVersion());
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
         }
     }
 
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetContains.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetContains.java
new file mode 100644
index 0000000000..c3e6cc7e5e
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetContains.java
@@ -0,0 +1,119 @@
+/*
+ * 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.jena.mem2.set.triple;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.HashSet;
+import java.util.List;
+
+
+@State(Scope.Benchmark)
+public class TestSetContains {
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+            "../testing/BSBM/bsbm-1m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "HashSet",
+            "HashCommonTripleSet",
+            "FastHashTripleSet"
+    })
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Boolean> setContains;
+    private List<Triple> triplesToFind;
+    private HashSet<Triple> tripleHashSet;
+    private HashCommonTripleSet hashCommonTripleSet;
+    private FastHashTripleSet fastHashTripleSet;
+
+    @Benchmark
+    public boolean setContains() {
+        return setContains.get();
+    }
+
+    private boolean hashSetContains() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = tripleHashSet.contains(t);
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean hashCommonTripleSetContains() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = hashCommonTripleSet.containsKey(t);
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    private boolean fastHashTripleSetContains() {
+        var found = false;
+        for (var t : triplesToFind) {
+            found = fastHashTripleSet.containsKey(t);
+            Assert.assertTrue(found);
+        }
+        return found;
+    }
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        var triples = Releases.current.readTriples(param0_GraphUri);
+        this.triplesToFind = Releases.current.cloneTriples(triples);
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.tripleHashSet = new HashSet<>(triples.size());
+                triples.forEach(tripleHashSet::add);
+                this.setContains = this::hashSetContains;
+                break;
+            case "HashCommonTripleSet":
+                this.hashCommonTripleSet = new HashCommonTripleSet(triples.size());
+                triples.forEach(hashCommonTripleSet::addUnchecked);
+                this.setContains = this::hashCommonTripleSetContains;
+                break;
+            case "FastHashTripleSet":
+                this.fastHashTripleSet = new FastHashTripleSet(triples.size());
+                triples.forEach(fastHashTripleSet::addUnchecked);
+                this.setContains = this::fastHashTripleSetContains;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetIterate.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetIterate.java
new file mode 100644
index 0000000000..5eef31e505
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetIterate.java
@@ -0,0 +1,125 @@
+/*
+ * 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.jena.mem2.set.triple;
+
+import org.apache.jena.atlas.iterator.ActionCount;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+
+@State(Scope.Benchmark)
+public class TestSetIterate {
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+            "../testing/BSBM/bsbm-1m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "HashSet",
+            "HashCommonTripleSet",
+            "FastHashTripleSet"
+    })
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Iterator<Triple>> getIterator;
+    private List<Triple> triples;
+    private HashSet<Triple> hashSet;
+    private HashCommonTripleSet hashCommonTripleSet;
+    private FastHashTripleSet fastHashTripleSet;
+
+    @Benchmark
+    public Object foreachRemaining() {
+        var it = getIterator.get();
+        ActionCount<Triple> counter = new ActionCount<>();
+        it.forEachRemaining(counter);
+        assertEquals(triples.size(), counter.getCount());
+        return counter;
+    }
+
+    @Benchmark
+    public Object hasNextNext() {
+        var it = getIterator.get();
+        int i = 0;
+        while (it.hasNext()) {
+            it.next();
+            i++;
+        }
+        assertEquals(triples.size(), i);
+        return i;
+    }
+
+    private Iterator<Triple> getIteratorFromHashSet() {
+        return hashSet.iterator();
+    }
+
+    private Iterator<Triple> getIteratorFromHashCommonTripleSet() {
+        return hashCommonTripleSet.keyIterator();
+    }
+
+    private Iterator<Triple> getIteratorFromFastHashTripleSet() {
+        return fastHashTripleSet.keyIterator();
+    }
+
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        this.triples = Releases.current.readTriples(param0_GraphUri);
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.hashSet = new HashSet<>(triples.size());
+                triples.forEach(hashSet::add);
+                this.getIterator = this::getIteratorFromHashSet;
+                break;
+            case "HashCommonTripleSet":
+                this.hashCommonTripleSet = new HashCommonTripleSet(triples.size());
+                triples.forEach(hashCommonTripleSet::addUnchecked);
+                this.getIterator = this::getIteratorFromHashCommonTripleSet;
+                break;
+            case "FastHashTripleSet":
+                this.fastHashTripleSet = new FastHashTripleSet(triples.size());
+                triples.forEach(fastHashTripleSet::addUnchecked);
+                this.getIterator = this::getIteratorFromFastHashTripleSet;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+
+}
\ No newline at end of file
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphMemoryConsumption.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetMemoryConsumption.java
similarity index 55%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphMemoryConsumption.java
copy to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetMemoryConsumption.java
index dd59e405f4..ff04809714 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/TestGraphMemoryConsumption.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetMemoryConsumption.java
@@ -16,11 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.jena.mem.graph;
+package org.apache.jena.mem2.set.triple;
 
 import org.apache.commons.lang3.time.StopWatch;
 import org.apache.jena.graph.Triple;
-import org.apache.jena.mem.graph.helper.Context;
 import org.apache.jena.mem.graph.helper.JMHDefaultOptions;
 import org.apache.jena.mem.graph.helper.Releases;
 import org.junit.Assert;
@@ -29,10 +28,11 @@ import org.openjdk.jmh.annotations.*;
 import org.openjdk.jmh.runner.Runner;
 
 import java.math.BigDecimal;
+import java.util.HashSet;
 import java.util.List;
 
 @State(Scope.Benchmark)
-public class TestGraphMemoryConsumption {
+public class TestSetMemoryConsumption {
 
     @Param({
             "../testing/cheeses-0.1.ttl",
@@ -42,76 +42,78 @@ public class TestGraphMemoryConsumption {
     public String param0_GraphUri;
 
     @Param({
-            "GraphMem (current)",
-            "GraphMem (Jena 4.8.0)",
+            "HashSet",
+            "HashCommonTripleSet",
+            "FastHashTripleSet"
     })
-    public String param1_GraphImplementation;
-    private Context trialContext;
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Object> fillSet;
+    private List<Triple> triples;
 
-    private List<Triple> allTriplesCurrent;
-    private List<org.apache.shadedJena480.graph.Triple> allTriples480;
-
-    java.util.function.Supplier<Object> graphFill;
-
-    @Benchmark
-    public Object graphFill() {
-        return graphFill.get();
+    /**
+     * This method is used to get the memory consumption of the current JVM.
+     *
+     * @return the memory consumption in MB
+     */
+    private static double runGcAndGetUsedMemoryInMB() {
+        System.runFinalization();
+        System.gc();
+        Runtime.getRuntime().runFinalization();
+        Runtime.getRuntime().gc();
+        return BigDecimal.valueOf(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()).divide(BigDecimal.valueOf(1024L)).divide(BigDecimal.valueOf(1024L)).doubleValue();
     }
 
-    private Object graphFillCurrent() {
+    @Benchmark
+    public Object fillSet() {
         var memoryBefore = runGcAndGetUsedMemoryInMB();
         var stopwatch = StopWatch.createStarted();
-        var sut = Releases.current.createGraph(trialContext.getGraphClass());
-        allTriplesCurrent.forEach(sut::add);
+        var sut = fillSet.get();
         stopwatch.stop();
         var memoryAfter = runGcAndGetUsedMemoryInMB();
-        System.out.println(String.format("graphs: %d time to fill graphs: %s additional memory: %5.3f MB",
-                sut.size(),
+        System.out.printf("graphs: %d time to fill graphs: %s additional memory: %5.3f MB%n",
+                triples.size(),
                 stopwatch.formatTime(),
-                (memoryAfter - memoryBefore)));
+                (memoryAfter - memoryBefore));
         return sut;
     }
 
-    private Object graphFill480() {
-        var memoryBefore = runGcAndGetUsedMemoryInMB();
-        var stopwatch = StopWatch.createStarted();
-        var sut = Releases.v480.createGraph(trialContext.getGraphClass());
-        allTriples480.forEach(sut::add);
-        stopwatch.stop();
-        var memoryAfter = runGcAndGetUsedMemoryInMB();
-        System.out.println(String.format("graphs: %d time to fill graphs: %s additional memory: %5.3f MB",
-                sut.size(),
-                stopwatch.formatTime(),
-                (memoryAfter - memoryBefore)));
+    private Object fillHashSet() {
+        var sut = new HashSet<Triple>();
+        triples.forEach(sut::add);
+        Assert.assertEquals(triples.size(), sut.size());
         return sut;
     }
 
-    /**
-     * This method is used to get the memory consumption of the current JVM.
-     * @return the memory consumption in MB
-     */
-    private static double runGcAndGetUsedMemoryInMB() {
-        System.runFinalization();
-        System.gc();
-        Runtime.getRuntime().runFinalization();
-        Runtime.getRuntime().gc();
-        return BigDecimal.valueOf(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()).divide(BigDecimal.valueOf(1024l)).divide(BigDecimal.valueOf(1024l)).doubleValue();
+    private Object fillHashCommonTripleSet() {
+        var sut = new HashCommonTripleSet();
+        triples.forEach(sut::addUnchecked);
+        Assert.assertEquals(triples.size(), sut.size());
+        return sut;
+    }
+
+    private Object fillFastHashTripleSet() {
+        var sut = new FastHashTripleSet();
+        triples.forEach(sut::addUnchecked);
+        Assert.assertEquals(triples.size(), sut.size());
+        return sut;
     }
 
+
     @Setup(Level.Trial)
     public void setupTrial() throws Exception {
-        this.trialContext = new Context(param1_GraphImplementation);
-        switch (this.trialContext.getJenaVersion()) {
-            case CURRENT:
-                this.allTriplesCurrent = Releases.current.readTriples(param0_GraphUri);
-                this.graphFill = this::graphFillCurrent;
+        triples = Releases.current.readTriples(param0_GraphUri);
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.fillSet = this::fillHashSet;
+                break;
+            case "HashCommonTripleSet":
+                this.fillSet = this::fillHashCommonTripleSet;
                 break;
-            case JENA_4_8_0:
-                this.allTriples480 = Releases.v480.readTriples(param0_GraphUri);
-                this.graphFill = this::graphFill480;
+            case "FastHashTripleSet":
+                this.fillSet = this::fillFastHashTripleSet;
                 break;
             default:
-                throw new IllegalArgumentException("Unknown Jena version: " + this.trialContext.getJenaVersion());
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
         }
     }
 
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetRemove.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetRemove.java
new file mode 100644
index 0000000000..56436e1be4
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetRemove.java
@@ -0,0 +1,124 @@
+/*
+ * 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.jena.mem2.set.triple;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.HashSet;
+import java.util.List;
+
+@State(Scope.Benchmark)
+public class TestSetRemove {
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+            "../testing/BSBM/bsbm-1m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "HashSet",
+            "HashCommonTripleSet",
+            "FastHashTripleSet"
+    })
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Integer> removeFromSet;
+    private List<Triple> triples;
+    private List<Triple> triplesToRemove;
+    private HashSet<Triple> hashSet;
+    private HashCommonTripleSet hashCommonTripleSet;
+    private FastHashTripleSet fastHashTripleSet;
+
+    @Benchmark
+    public int setRemove() {
+        return removeFromSet.get();
+    }
+
+    private int removeFromHashSet() {
+        triplesToRemove.forEach(t -> this.hashSet.remove(t));
+        Assert.assertTrue(this.hashSet.isEmpty());
+        return this.hashSet.size();
+    }
+
+    private int removeFromHashCommonTripleSet() {
+        triplesToRemove.forEach(t -> this.hashCommonTripleSet.removeUnchecked(t));
+        Assert.assertTrue(this.hashCommonTripleSet.isEmpty());
+        return this.hashCommonTripleSet.size();
+    }
+
+    private int removeFromFastHashTripleSet() {
+        triplesToRemove.forEach(t -> this.fastHashTripleSet.removeUnchecked(t));
+        Assert.assertTrue(this.fastHashTripleSet.isEmpty());
+        return this.fastHashTripleSet.size();
+    }
+
+    @Setup(Level.Invocation)
+    public void setupInvocation() {
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.hashSet = new HashSet<>(triples.size());
+                this.triples.forEach(hashSet::add);
+                break;
+            case "HashCommonTripleSet":
+                this.hashCommonTripleSet = new HashCommonTripleSet(triples.size());
+                this.triples.forEach(hashCommonTripleSet::addUnchecked);
+                break;
+            case "FastHashTripleSet":
+                this.fastHashTripleSet = new FastHashTripleSet(triples.size());
+                this.triples.forEach(fastHashTripleSet::addUnchecked);
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        this.triples = Releases.current.readTriples(param0_GraphUri);
+        this.triplesToRemove = Releases.current.cloneTriples(triples);
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.removeFromSet = this::removeFromHashSet;
+                break;
+            case "HashCommonTripleSet":
+                this.removeFromSet = this::removeFromHashCommonTripleSet;
+                break;
+            case "FastHashTripleSet":
+                this.removeFromSet = this::removeFromFastHashTripleSet;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetStreamAll.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetStreamAll.java
new file mode 100644
index 0000000000..f9c5ef29d7
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetStreamAll.java
@@ -0,0 +1,120 @@
+/*
+ * 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.jena.mem2.set.triple;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static org.junit.Assert.assertEquals;
+
+
+@State(Scope.Benchmark)
+public class TestSetStreamAll {
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+            "../testing/BSBM/bsbm-1m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "HashSet",
+            "HashCommonTripleSet",
+            "FastHashTripleSet"
+    })
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Spliterator<Triple>> getSpliterator;
+    private List<Triple> triples;
+    private HashSet<Triple> hashSet;
+    private HashCommonTripleSet hashCommonTripleSet;
+    private FastHashTripleSet fastHashTripleSet;
+
+    @Benchmark
+    public Object streamSet() {
+        var list = StreamSupport.stream(getSpliterator.get(), false)
+                .collect(Collectors.toList());
+        assertEquals(triples.size(), list.size());
+        return list;
+    }
+
+    @Benchmark
+    public Object streamSetParallel() {
+        var list = StreamSupport.stream(getSpliterator.get(), true)
+                .collect(Collectors.toList());
+        assertEquals(triples.size(), list.size());
+        return list;
+    }
+
+    private Spliterator<Triple> getSpliteratorFromHashSet() {
+        return hashSet.spliterator();
+    }
+
+    private Spliterator<Triple> getSpliteratorFromHashCommonTripleSet() {
+        return hashCommonTripleSet.keySpliterator();
+    }
+
+    private Spliterator<Triple> getSpliteratorFromFastHashTripleSet() {
+        return fastHashTripleSet.keySpliterator();
+    }
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        this.triples = Releases.current.readTriples(param0_GraphUri);
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.hashSet = new HashSet<>(triples.size());
+                triples.forEach(hashSet::add);
+                this.getSpliterator = this::getSpliteratorFromHashSet;
+                break;
+            case "HashCommonTripleSet":
+                this.hashCommonTripleSet = new HashCommonTripleSet(triples.size());
+                triples.forEach(hashCommonTripleSet::addUnchecked);
+                this.getSpliterator = this::getSpliteratorFromHashCommonTripleSet;
+                break;
+            case "FastHashTripleSet":
+                this.fastHashTripleSet = new FastHashTripleSet(triples.size());
+                triples.forEach(fastHashTripleSet::addUnchecked);
+                this.getSpliterator = this::getSpliteratorFromFastHashTripleSet;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+
+}
\ No newline at end of file
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetUpdate.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetUpdate.java
new file mode 100644
index 0000000000..af885d4ceb
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/set/triple/TestSetUpdate.java
@@ -0,0 +1,143 @@
+/*
+ * 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.jena.mem2.set.triple;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem.graph.helper.Releases;
+import org.apache.jena.mem2.helper.JMHDefaultOptions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+
+@State(Scope.Benchmark)
+public class TestSetUpdate {
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+            "../testing/BSBM/bsbm-1m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "HashSet",
+            "HashCommonTripleSet",
+            "FastHashTripleSet"
+    })
+    public String param1_SetImplementation;
+    java.util.function.Supplier<Integer> updateSet;
+    private List<Triple> triples;
+    private List<Triple> triplesToRemove;
+    private HashSet<Triple> hashSet;
+    private HashCommonTripleSet hashCommonTripleSet;
+    private FastHashTripleSet fastHashTripleSet;
+
+    @Benchmark
+    public int updateSet() {
+        return updateSet.get();
+    }
+
+    private int updateHashSet() {
+        for (int i = 0; i < triplesToRemove.size(); i += 10) {
+            triplesToRemove.subList(i, Math.min(i + 10, triplesToRemove.size()))
+                    .forEach(t -> this.hashSet.remove(t));
+            triplesToRemove.subList(i, Math.min(i + 10, triplesToRemove.size()))
+                    .forEach(t -> this.hashSet.add(t));
+            assert this.hashSet.size() == triples.size();
+        }
+        return this.hashSet.size();
+    }
+
+    private int updateHashCommonTripleSet() {
+        for (int i = 0; i < triplesToRemove.size(); i += 10) {
+            triplesToRemove.subList(i, Math.min(i + 10, triplesToRemove.size()))
+                    .forEach(t -> this.hashCommonTripleSet.tryRemove(t));
+            triplesToRemove.subList(i, Math.min(i + 10, triplesToRemove.size()))
+                    .forEach(t -> this.hashCommonTripleSet.tryAdd(t));
+            assert this.hashCommonTripleSet.size() == triples.size();
+        }
+        return this.hashCommonTripleSet.size();
+    }
+
+
+    private int updateFastHashTripleSet() {
+        for (int i = 0; i < triplesToRemove.size(); i += 10) {
+            triplesToRemove.subList(i, Math.min(i + 10, triplesToRemove.size()))
+                    .forEach(t -> this.fastHashTripleSet.tryRemove(t));
+            triplesToRemove.subList(i, Math.min(i + 10, triplesToRemove.size()))
+                    .forEach(t -> this.fastHashTripleSet.tryAdd(t));
+            assert this.fastHashTripleSet.size() == triples.size();
+        }
+        return this.fastHashTripleSet.size();
+    }
+
+    @Setup(Level.Invocation)
+    public void setupInvocation() {
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.hashSet = new HashSet<>(triples.size());
+                this.triples.forEach(hashSet::add);
+                break;
+            case "HashCommonTripleSet":
+                this.hashCommonTripleSet = new HashCommonTripleSet(triples.size());
+                this.triples.forEach(hashCommonTripleSet::addUnchecked);
+                break;
+            case "FastHashTripleSet":
+                this.fastHashTripleSet = new FastHashTripleSet(triples.size());
+                this.triples.forEach(fastHashTripleSet::addUnchecked);
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        this.triples = Releases.current.readTriples(param0_GraphUri);
+        this.triplesToRemove = Releases.current.cloneTriples(triples);
+        Collections.shuffle(triplesToRemove, new Random(4721));
+        switch (param1_SetImplementation) {
+            case "HashSet":
+                this.updateSet = this::updateHashSet;
+                break;
+            case "HashCommonTripleSet":
+                this.updateSet = this::updateHashCommonTripleSet;
+                break;
+            case "FastHashTripleSet":
+                this.updateSet = this::updateFastHashTripleSet;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown set implementation: " + param1_SetImplementation);
+        }
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
similarity index 84%
rename from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
rename to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
index 9bd9eb5f9f..dc4f880504 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsForeachRemaining.java
@@ -16,11 +16,9 @@
  * limitations under the License.
  */
 
-package org.apache.jena.mem.spliterator;
+package org.apache.jena.mem2.spliterator;
 
 import org.apache.jena.atlas.iterator.ActionCount;
-import org.apache.jena.mem.SparseArraySpliterator;
-import org.apache.jena.mem.SparseArraySubSpliterator;
 import org.junit.Assert;
 import org.junit.Test;
 import org.openjdk.jmh.annotations.*;
@@ -41,26 +39,21 @@ import java.util.concurrent.TimeUnit;
 public class TestSparseArraySpliteratorsForeachRemaining {
 
 
-    final static int[] stepsWithNull = new int[] {1, 2, 3, 4, 5};
-
-    List<Object[]> arraysWithNulls = new ArrayList<>(stepsWithNull.length);
-
-    List<Integer> elementsCounts = new ArrayList<>(stepsWithNull.length);
-
-
+    final static int[] stepsWithNull = new int[]{1, 2, 3, 4, 5};
     @Param({"1000000", "2000000", "3000000", "5000000"})
     public int param0_arraySize;
-
     @Param({
-            "SparseArraySpliterator",
-            "SparseArraySubSpliterator",
+            "mem.SparseArraySpliterator",
+            "mem2.SparseArraySpliterator"
     })
     public String param1_iteratorImplementation;
+    List<Object[]> arraysWithNulls = new ArrayList<>(stepsWithNull.length);
+    List<Integer> elementsCounts = new ArrayList<>(stepsWithNull.length);
 
     @Benchmark
     public long testSpliteratorForeachRemaining() {
         long total = 0;
-        for(int i = 0; i < stepsWithNull.length; i++) {
+        for (int i = 0; i < stepsWithNull.length; i++) {
             var arrayWithNulls = arraysWithNulls.get(i);
             var elementsCount = elementsCounts.get(i);
             var actionCounter = new ActionCount<>();
@@ -83,12 +76,10 @@ public class TestSparseArraySpliteratorsForeachRemaining {
             }
         };
         switch (param1_iteratorImplementation) {
-            case "SparseArraySpliterator":
-                return new SparseArraySpliterator<>(arrayWithNulls, 0, checkForConcurrentModification);
-
-            case "SparseArraySubSpliterator":
-                return new SparseArraySubSpliterator<>(arrayWithNulls, 0, checkForConcurrentModification);
-
+            case "mem.SparseArraySpliterator":
+                return new org.apache.jena.mem.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification);
+            case "mem2.SparseArraySpliterator":
+                return new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification);
             default:
                 throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation);
         }
@@ -96,11 +87,11 @@ public class TestSparseArraySpliteratorsForeachRemaining {
 
     @Setup(Level.Trial)
     public void setupTrial() throws Exception {
-        for(int i = 0; i < stepsWithNull.length; i++) {
+        for (int i = 0; i < stepsWithNull.length; i++) {
             var arrayWithNulls = new Object[param0_arraySize];
-            var stepsWithNull = this.stepsWithNull[i];
+            var stepsWithNull = TestSparseArraySpliteratorsForeachRemaining.stepsWithNull[i];
             var elementsCount = 0;
-            for (int k = 0; k < arrayWithNulls.length; k+=1+ stepsWithNull) {
+            for (int k = 0; k < arrayWithNulls.length; k += 1 + stepsWithNull) {
                 arrayWithNulls[k] = new Object();
                 elementsCount++;
             }
@@ -116,7 +107,7 @@ public class TestSparseArraySpliteratorsForeachRemaining {
                 // You can be more specific if you'd like to run only one benchmark per test.
                 .include(this.getClass().getName())
                 // Set the following options as needed
-                .mode (Mode.AverageTime)
+                .mode(Mode.AverageTime)
                 .timeUnit(TimeUnit.SECONDS)
                 .warmupTime(TimeValue.NONE)
                 .warmupIterations(10)
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsStreamParallel.java
similarity index 84%
rename from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java
rename to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsStreamParallel.java
index 146cff16ee..0f9732e832 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsStreamParallel.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsStreamParallel.java
@@ -16,11 +16,9 @@
  * limitations under the License.
  */
 
-package org.apache.jena.mem.spliterator;
+package org.apache.jena.mem2.spliterator;
 
 import org.apache.jena.atlas.iterator.ActionCount;
-import org.apache.jena.mem.SparseArraySpliterator;
-import org.apache.jena.mem.SparseArraySubSpliterator;
 import org.junit.Assert;
 import org.junit.Test;
 import org.openjdk.jmh.annotations.*;
@@ -42,26 +40,21 @@ import java.util.stream.StreamSupport;
 public class TestSparseArraySpliteratorsStreamParallel {
 
 
-    final static int[] stepsWithNull = new int[] {1, 2, 3, 4, 5};
-
-    List<Object[]> arraysWithNulls = new ArrayList<>(stepsWithNull.length);
-
-    List<Integer> elementsCounts = new ArrayList<>(stepsWithNull.length);
-
-
+    final static int[] stepsWithNull = new int[]{1, 2, 3, 4, 5};
     @Param({"1000000", "2000000", "3000000", "5000000"})
     public int param0_arraySize;
-
     @Param({
-            "SparseArraySpliterator",
-            "SparseArraySubSpliterator",
+            "mem.SparseArraySpliterator",
+            "mem2.SparseArraySpliterator"
     })
     public String param1_iteratorImplementation;
+    List<Object[]> arraysWithNulls = new ArrayList<>(stepsWithNull.length);
+    List<Integer> elementsCounts = new ArrayList<>(stepsWithNull.length);
 
     @Benchmark
     public long testSpliteratorForeachRemaining() {
         long total = 0;
-        for(int i = 0; i < stepsWithNull.length; i++) {
+        for (int i = 0; i < stepsWithNull.length; i++) {
             var arrayWithNulls = arraysWithNulls.get(i);
             var elementsCount = elementsCounts.get(i);
             var actionCounter = new ActionCount<>();
@@ -84,11 +77,10 @@ public class TestSparseArraySpliteratorsStreamParallel {
             }
         };
         switch (param1_iteratorImplementation) {
-            case "SparseArraySpliterator":
-                return new SparseArraySpliterator<>(arrayWithNulls, 0, checkForConcurrentModification);
-
-            case "SparseArraySubSpliterator":
-                return new SparseArraySubSpliterator<>(arrayWithNulls, 0, checkForConcurrentModification);
+            case "mem.SparseArraySpliterator":
+                return new org.apache.jena.mem.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification);
+            case "mem2.SparseArraySpliterator":
+                return new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification);
 
             default:
                 throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation);
@@ -97,11 +89,11 @@ public class TestSparseArraySpliteratorsStreamParallel {
 
     @Setup(Level.Trial)
     public void setupTrial() throws Exception {
-        for(int i = 0; i < stepsWithNull.length; i++) {
+        for (int i = 0; i < stepsWithNull.length; i++) {
             var arrayWithNulls = new Object[param0_arraySize];
-            var stepsWithNull = this.stepsWithNull[i];
+            var stepsWithNull = TestSparseArraySpliteratorsStreamParallel.stepsWithNull[i];
             var elementsCount = 0;
-            for (int k = 0; k < arrayWithNulls.length; k+=1+ stepsWithNull) {
+            for (int k = 0; k < arrayWithNulls.length; k += 1 + stepsWithNull) {
                 arrayWithNulls[k] = new Object();
                 elementsCount++;
             }
@@ -117,7 +109,7 @@ public class TestSparseArraySpliteratorsStreamParallel {
                 // You can be more specific if you'd like to run only one benchmark per test.
                 .include(this.getClass().getName())
                 // Set the following options as needed
-                .mode (Mode.AverageTime)
+                .mode(Mode.AverageTime)
                 .timeUnit(TimeUnit.SECONDS)
                 .warmupTime(TimeValue.NONE)
                 .warmupIterations(10)
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsTryAdvance.java
similarity index 80%
rename from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java
rename to jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsTryAdvance.java
index 7335fc0db1..39ba80940a 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/spliterator/TestSparseArraySpliteratorsTryAdvance.java
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem2/spliterator/TestSparseArraySpliteratorsTryAdvance.java
@@ -16,11 +16,9 @@
  * limitations under the License.
  */
 
-package org.apache.jena.mem.spliterator;
+package org.apache.jena.mem2.spliterator;
 
 import org.apache.jena.atlas.iterator.ActionCount;
-import org.apache.jena.mem.SparseArraySpliterator;
-import org.apache.jena.mem.SparseArraySubSpliterator;
 import org.junit.Assert;
 import org.junit.Test;
 import org.openjdk.jmh.annotations.*;
@@ -41,33 +39,29 @@ import java.util.concurrent.TimeUnit;
 public class TestSparseArraySpliteratorsTryAdvance {
 
 
-    final static int[] stepsWithNull = new int[] {1, 2, 3, 4, 5};
-
-    List<Object[]> arraysWithNulls = new ArrayList<>(stepsWithNull.length);
-
-    List<Integer> elementsCounts = new ArrayList<>(stepsWithNull.length);
-
-
+    final static int[] stepsWithNull = new int[]{1, 2, 3, 4, 5};
     @Param({"1000000", "2000000", "3000000", "5000000"})
     public int param0_arraySize;
-
     @Param({
-            "SparseArraySpliterator",
-            "SparseArraySubSpliterator",
+            "mem.SparseArraySpliterator",
+            "mem2.SparseArraySpliterator"
     })
     public String param1_iteratorImplementation;
+    List<Object[]> arraysWithNulls = new ArrayList<>(stepsWithNull.length);
+    List<Integer> elementsCounts = new ArrayList<>(stepsWithNull.length);
 
     @Benchmark
     public long testSpliteratorTryAdvance() {
         long total = 0;
-        for(int i = 0; i < stepsWithNull.length; i++) {
+        for (int i = 0; i < stepsWithNull.length; i++) {
             var arrayWithNulls = arraysWithNulls.get(i);
             var elementsCount = elementsCounts.get(i);
             var actionCounter = new ActionCount<>();
 
             var sut = createSut(arrayWithNulls, elementsCount);
 
-            do {} while (sut.tryAdvance(actionCounter::accept));
+            do {
+            } while (sut.tryAdvance(actionCounter::accept));
 
             total += actionCounter.getCount();
             Assert.assertEquals(elementsCount.longValue(), actionCounter.getCount());
@@ -75,12 +69,6 @@ public class TestSparseArraySpliteratorsTryAdvance {
         return total;
     }
 
-    private long count(Spliterator<Object> spliterator) {
-        var actionCount = new ActionCount<>();
-        spliterator.forEachRemaining(actionCount::accept);
-        return actionCount.getCount();
-    }
-
 
     public Spliterator<Object> createSut(Object[] arrayWithNulls, int elementsCount) {
         var count = elementsCount;
@@ -90,11 +78,10 @@ public class TestSparseArraySpliteratorsTryAdvance {
             }
         };
         switch (param1_iteratorImplementation) {
-            case "SparseArraySpliterator":
-                return new SparseArraySpliterator<>(arrayWithNulls, 0, checkForConcurrentModification);
-
-            case "SparseArraySubSpliterator":
-                return new SparseArraySubSpliterator<>(arrayWithNulls, 0, checkForConcurrentModification);
+            case "mem.SparseArraySpliterator":
+                return new org.apache.jena.mem.SparseArraySpliterator<>(arrayWithNulls, count, checkForConcurrentModification);
+            case "mem2.SparseArraySpliterator":
+                return new SparseArraySpliterator<>(arrayWithNulls, checkForConcurrentModification);
 
             default:
                 throw new IllegalArgumentException("Unknown spliterator implementation: " + param1_iteratorImplementation);
@@ -103,11 +90,11 @@ public class TestSparseArraySpliteratorsTryAdvance {
 
     @Setup(Level.Trial)
     public void setupTrial() throws Exception {
-        for(int i = 0; i < stepsWithNull.length; i++) {
+        for (int i = 0; i < stepsWithNull.length; i++) {
             var arrayWithNulls = new Object[param0_arraySize];
-            var stepsWithNull = this.stepsWithNull[i];
+            var stepsWithNull = TestSparseArraySpliteratorsTryAdvance.stepsWithNull[i];
             var elementsCount = 0;
-            for (int k = 0; k < arrayWithNulls.length; k+=1+ stepsWithNull) {
+            for (int k = 0; k < arrayWithNulls.length; k += 1 + stepsWithNull) {
                 arrayWithNulls[k] = new Object();
                 elementsCount++;
             }
@@ -123,7 +110,7 @@ public class TestSparseArraySpliteratorsTryAdvance {
                 // You can be more specific if you'd like to run only one benchmark per test.
                 .include(this.getClass().getName())
                 // Set the following options as needed
-                .mode (Mode.AverageTime)
+                .mode(Mode.AverageTime)
                 .timeUnit(TimeUnit.SECONDS)
                 .warmupTime(TimeValue.NONE)
                 .warmupIterations(10)
diff --git a/jena-core/pom.xml b/jena-core/pom.xml
index 37b7adfe85..0ef347ce5d 100644
--- a/jena-core/pom.xml
+++ b/jena-core/pom.xml
@@ -53,25 +53,30 @@
     </dependency>
 
     <dependency>
-      <groupId>org.apache.jena</groupId>
-      <artifactId>jena-iri</artifactId>
-      <version>4.9.0-SNAPSHOT</version>
+        <groupId>org.apache.jena</groupId>
+        <artifactId>jena-iri</artifactId>
+        <version>4.9.0-SNAPSHOT</version>
     </dependency>
 
-    <dependency>
-       <artifactId>commons-cli</artifactId>
-       <groupId>commons-cli</groupId>
-     </dependency>
-
-     <dependency>
-       <groupId>junit</groupId>
-       <artifactId>junit</artifactId>
-       <scope>test</scope>
-    </dependency>
-    
-    <dependency>
-      <groupId>org.xenei</groupId>
-      <artifactId>junit-contracts</artifactId>
+      <dependency>
+          <artifactId>commons-cli</artifactId>
+          <groupId>commons-cli</groupId>
+      </dependency>
+
+      <dependency>
+          <groupId>org.roaringbitmap</groupId>
+          <artifactId>RoaringBitmap</artifactId>
+      </dependency>
+
+      <dependency>
+          <groupId>junit</groupId>
+          <artifactId>junit</artifactId>
+          <scope>test</scope>
+      </dependency>
+
+      <dependency>
+          <groupId>org.xenei</groupId>
+          <artifactId>junit-contracts</artifactId>
       <scope>test</scope>
       <exclusions>
         <exclusion>
diff --git a/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java b/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java
index a9b082eeb1..32327de109 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/GraphMem.java
@@ -27,18 +27,23 @@ import java.util.stream.Stream;
 /** @deprecated This implementation of GraphMem will be replaced by a new implementation at Jena 4.6.0.
  *   Application should be using {@link GraphMemFactory#createDefaultGraph()} for a general purpose graph or {@link GraphMemFactory#createGraphMem()}
  *   to specific this style of implementation.
-*/
+ */
 @Deprecated
-public class GraphMem extends GraphMemBase
-{
-    public GraphMem()
-    { super(  ); }
+public class GraphMem extends GraphMemBase {
+    /**
+     This Graph's TripleStore. Visible for <i>read-only</i> purposes only.
+     */
+    public final TripleStore store;
 
-    @Override protected TripleStore createTripleStore()
-    { return new GraphTripleStoreMem( this ); }
+    public GraphMem() {
+        super();
+        store = new GraphTripleStoreMem(this);
+    }
 
-    @Override protected void destroy()
-    { store.close(); }
+    @Override
+    protected void destroy() {
+        store.close();
+    }
 
     @Override public void performAdd( Triple t )
     { store.add( t ); }
diff --git a/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java b/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java
index 39ffd1d27d..e8abf6fe06 100644
--- a/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java
+++ b/jena-core/src/main/java/org/apache/jena/mem/GraphMemBase.java
@@ -18,9 +18,8 @@
 
 package org.apache.jena.mem;
 
-import org.apache.jena.graph.Triple ;
-import org.apache.jena.graph.impl.GraphBase ;
-import org.apache.jena.graph.impl.TripleStore ;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.graph.impl.GraphBase;
 
 /**
      GraphMemBase - a common base class for GraphMem and SmallGraphMem.
@@ -38,23 +37,14 @@ public abstract class GraphMemBase extends GraphBase
          The number-of-times-opened count.
     */
     protected int count;
-    
-    /**
-        This Graph's TripleStore. Visible for <i>read-only</i> purposes only.
-    */
-    public final TripleStore store;
-    
+
     /**
          initialise a GraphMemBase with its count set to 1.
     */
     public GraphMemBase( )
     {
-        store = createTripleStore();
-        count = 1; 
+        count = 1;
     }
-    
-    protected abstract TripleStore createTripleStore();
-    
     /**
          Note a re-opening of this graph by incrementing the count. Answer
          this Graph.
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2.java b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2.java
new file mode 100644
index 0000000000..5f78988a73
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2.java
@@ -0,0 +1,169 @@
+/*
+ * 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.jena.mem2;
+
+import org.apache.jena.graph.Capabilities;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.graph.impl.GraphWithPerform;
+import org.apache.jena.mem.GraphMemBase;
+import org.apache.jena.mem2.store.TripleStore;
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.stream.Stream;
+
+/**
+ * A graph that stores triples in memory. This class is not thread-safe.
+ * All triples are stored in a {@link TripleStore}.
+ * <p>
+ * Implementation mus always comply to term-equality semantics. The characteristics of the
+ * implementations always have handlesLiteralTyping() == false.
+ */
+public class GraphMem2 extends GraphMemBase implements GraphWithPerform {
+
+    final TripleStore tripleStore;
+
+    public GraphMem2(TripleStore tripleStore) {
+        super();
+        this.tripleStore = tripleStore;
+    }
+
+    /**
+     * Subclasses over-ride this method to release any resources they no
+     * longer need once fully closed.
+     */
+    @Override
+    public void destroy() {
+        this.tripleStore.clear();
+    }
+
+    /**
+     * Remove all the statements from this graph.
+     */
+    @Override
+    public void clear() {
+        super.clear(); /* deletes all triples and sends notifications*/
+        this.tripleStore.clear();
+    }
+
+    /**
+     * Add a triple to the graph without notifying. The default implementation throws an
+     * AddDeniedException; subclasses must override if they want to be able to
+     * add triples.
+     *
+     * @param t triple to add
+     */
+    @Override
+    public void performAdd(final Triple t) {
+        tripleStore.add(t);
+    }
+
+    /**
+     * Remove a triple from the triple store. The default implementation throws
+     * a DeleteDeniedException; subclasses must override if they want to be able
+     * to remove triples.
+     *
+     * @param t triple to delete
+     */
+    @Override
+    public void performDelete(Triple t) {
+        tripleStore.remove(t);
+    }
+
+    /**
+     * Returns a {@link Stream} of all triples in the graph.
+     * Note: {@link Stream#parallel()} is supported.
+     *
+     * @return a stream  of triples in this graph.
+     */
+    @Override
+    public Stream<Triple> stream() {
+        return this.tripleStore.stream();
+    }
+
+    /**
+     * Returns a {@link Stream} of Triples matching a pattern.
+     * Note: {@link Stream#parallel()} is supported.
+     *
+     * @param sm subject node match pattern
+     * @param pm predicate node match pattern
+     * @param om object node match pattern
+     * @return a stream  of triples in this graph matching the pattern.
+     */
+    @Override
+    public Stream<Triple> stream(final Node sm, final Node pm, final Node om) {
+        return this.tripleStore.stream(Triple.createMatch(sm, pm, om));
+    }
+
+    /**
+     * Returns an {@link ExtendedIterator} of all triples in the graph matching the given triple match.
+     */
+    @Override
+    public ExtendedIterator<Triple> graphBaseFind(Triple tripleMatch) {
+        return this.tripleStore.find(tripleMatch);
+    }
+
+    /**
+     * Answer true if the graph contains any triple matching <code>t</code>.
+     * The default implementation uses <code>find</code> and checks to see
+     * if the iterator is non-empty.
+     *
+     * @param tripleMatch triple match pattern, which may be contained
+     */
+    @Override
+    public boolean graphBaseContains(final Triple tripleMatch) {
+        return this.tripleStore.contains(tripleMatch);
+    }
+
+    /**
+     * Answer the number of triples in this graph. Default implementation counts its
+     * way through the results of a findAll. Subclasses must override if they want
+     * size() to be efficient.
+     */
+    @Override
+    public int graphBaseSize() {
+        return this.tripleStore.countTriples();
+    }
+
+    @Override
+    public Capabilities getCapabilities() {
+        if (capabilities == null) capabilities = new Capabilities() {
+            @Override
+            public boolean sizeAccurate() {
+                return true;
+            }
+
+            @Override
+            public boolean addAllowed() {
+                return true;
+            }
+
+            @Override
+            public boolean deleteAllowed() {
+                return true;
+            }
+
+            @Override
+            public boolean handlesLiteralTyping() {
+                return false;
+            }
+        };
+        return capabilities;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Fast.java b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Fast.java
new file mode 100644
index 0000000000..0e4d2efbab
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Fast.java
@@ -0,0 +1,45 @@
+/*
+ * 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.jena.mem2;
+
+import org.apache.jena.mem2.store.fast.FastTripleStore;
+
+/**
+ * A graph that stores triples in memory. This class is not thread-safe.
+ * <p>
+ * Purpose: GraphMem2Fast is a strong candidate for becoming the new default in-memory graph in the upcoming Jena 5,
+ * thanks to its improved performance and relatively minor increase in memory usage.
+ * <p>
+ * Faster than {@link GraphMem2Legacy} (specially Graph#add, Graph#find and Graph#stream)
+ * Removing triples is a bit slower than {@link GraphMem2Legacy}.
+ * Memory consumption is about 6-35% higher than {@link GraphMem2Legacy}
+ * Maps and sets are based on {@link org.apache.jena.mem2.collection.FastHashBase}
+ * Benefits from multiple small optimizations. (see: {@link FastTripleStore})
+ * <p>
+ * The heritage of GraphMem:
+ * - Also uses 3 hash-maps indexed by subjects, predicates, and objects
+ * - Values of the maps also switch from arrays to hash sets for the triples
+ */
+public class GraphMem2Fast extends GraphMem2 {
+
+    public GraphMem2Fast() {
+        super(new FastTripleStore());
+    }
+
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Legacy.java b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Legacy.java
new file mode 100644
index 0000000000..9a182c894e
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Legacy.java
@@ -0,0 +1,47 @@
+/*
+ * 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.jena.mem2;
+
+import org.apache.jena.mem2.store.legacy.LegacyTripleStore;
+
+/**
+ * A graph that stores triples in memory. This class is not thread-safe.
+ * <p>
+ * Purpose: Use this graph implementation if you want to maintain the 'old' behavior of GraphMem or if your memory
+ * constraints prevent you from utilizing more memory-intensive solutions.
+ * <p>
+ * Slightly improved performance compared to {@link org.apache.jena.mem.GraphMem}
+ * Simplified implementation, primarily due to lack of support for Iterator#remove
+ * <p>
+ * The heritage of GraphMem:
+ * - Same basic structure
+ * - Same memory consumption
+ * - Also based on HashCommon
+ * <p>
+ * This implementation is based on the original {@link org.apache.jena.mem.GraphMem} implementation.
+ * The main difference is that it strictly uses term equality for all nodes.
+ * The inner workings of the used structures like ArrayBunch and HashedBunchMap are not changed.
+ */
+public class GraphMem2Legacy extends GraphMem2 {
+
+    public GraphMem2Legacy() {
+        super(new LegacyTripleStore());
+    }
+
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Roaring.java b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Roaring.java
new file mode 100644
index 0000000000..c37f5579d5
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/GraphMem2Roaring.java
@@ -0,0 +1,49 @@
+/*
+ * 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.jena.mem2;
+
+import org.apache.jena.mem2.store.roaring.RoaringTripleStore;
+
+/**
+ * A graph that stores triples in memory. This class is not thread-safe.
+ * <p>
+ * Purpose: GraphMem2Roaring is ideal for handling extremely large graphs. If you frequently work with such massive
+ * data structures, this implementation could be your top choice.
+ * <p>
+ * Graph#contains is faster than {@link GraphMem2Fast}.
+ * Removing triples is a bit slower than {@link GraphMem2Legacy}.
+ * Better performance than GraphMem2Fast for operations with triple matches for the pattern S_O, SP_, and _PO on
+ * large graphs,due to bit-operations to find intersecting triples.
+ * Memory consumption is about 7-99% higher than {@link GraphMem2Legacy}
+ * Suitable for really large graphs like bsbm-5m.nt.gz, bsbm-25m.nt.gz, and possibly even larger.
+ * Simple and straightforward implementation.
+ * No heritage of GraphMem.
+ * <p>
+ * Internal structure:
+ * - One indexed hash set (same as GraphMem2Fast uses) that holds all triples.
+ * - Three hash maps indexed by subjects, predicates, and objects with RoaringBitmaps as values.
+ * - The bitmaps contain the indices of the triples in the central hash set.
+ */
+public class GraphMem2Roaring extends GraphMem2 {
+
+    public GraphMem2Roaring() {
+        super(new RoaringTripleStore());
+    }
+
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashBase.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashBase.java
new file mode 100644
index 0000000000..aa2823a1f2
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashBase.java
@@ -0,0 +1,422 @@
+/*
+ * 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.jena.mem2.collection;
+
+import org.apache.jena.mem2.iterator.SparseArrayIterator;
+import org.apache.jena.mem2.spliterator.SparseArraySpliterator;
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.ConcurrentModificationException;
+import java.util.Spliterator;
+import java.util.function.Predicate;
+
+/**
+ * This is the base class for {@link FastHashSet} and {@link FastHashSet}.
+ * It only grows but never shrinks.
+ * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order.
+ * This map does not allow null keys.
+ * This map is not thread safe.
+ * <p>
+ * The positions array stores negative indices to the entries and hashCode arrays.
+ * The positions array is implemented as a power of two sized array. (like in {@link java.util.HashMap}) This allows
+ * to use a fast modulo operation to calculate the index. The indices of the positions array are derived from the
+ * hashCodes.
+ * Any position 0 indicates an empty element. The comparison with 0 is faster than comparing elements with null.
+ * <p>
+ * The keys are stored in a keys array and the hashCodesOrDeletedIndices array
+ * stores the hashCodes of the keys.
+ * hashCodesOrDeletedIndices is also used to store the indices of the deleted keys to save memory. It works like a
+ * linked list of deleted keys. The index of the previously deleted key is stored in the hashCodesOrDeletedIndices
+ * array. lastDeletedIndex is the index of the last deleted key in the hashCodesOrDeletedIndices array and serves as
+ * the head of the linked list of deleted keys.
+ * These two arrays grow together. They grow like {@link java.util.ArrayList} with a factor of 1.5.
+ * <p>
+ * keysPos is the index of the next free position in the keys array.
+ * The keys array is usually completely filled from index 0 to keysPos. Exceptions are the deleted keys.
+ * Indices that have been deleted are reused for new keys before the keys array is extended.
+ * The dense nature of the keys array enables fast iteration.
+ * <p>
+ * The index of a key in the keys array never changes. So the index of a key can be used as a handle to the key and
+ * for random access.
+ *
+ * @param <K> the type of the keys
+ */
+public abstract class FastHashBase<K> implements JenaMapSetCommon<K> {
+    protected static final int MINIMUM_HASHES_SIZE = 16;
+    protected static final int MINIMUM_ELEMENTS_SIZE = 8;
+    protected int keysPos = 0;
+    protected K[] keys;
+    protected int[] hashCodesOrDeletedIndices;
+    protected int lastDeletedIndex = -1;
+    protected int removedKeysCount = 0;
+
+    /**
+     * The negative indices to the entries and hashCode arrays.
+     * The indices of the positions array are derived from the hashCodes.
+     * Any position 0 indicates an empty element.
+     */
+    protected int[] positions;
+
+    protected FastHashBase(int initialSize) {
+        var positionsSize = Integer.highestOneBit(initialSize << 1);
+        if (positionsSize < initialSize << 1) {
+            positionsSize <<= 1;
+        }
+        this.positions = new int[positionsSize];
+        this.keys = newKeysArray(initialSize);
+        this.hashCodesOrDeletedIndices = new int[initialSize];
+    }
+
+    protected FastHashBase() {
+        this.positions = new int[MINIMUM_HASHES_SIZE];
+        this.keys = newKeysArray(MINIMUM_ELEMENTS_SIZE);
+        this.hashCodesOrDeletedIndices = new int[MINIMUM_ELEMENTS_SIZE];
+
+    }
+
+    /**
+     * Gets a new array of keys with the given size.
+     *
+     * @param size the size of the array
+     * @return the new array
+     */
+    protected abstract K[] newKeysArray(int size);
+
+    /**
+     * Calculates a position in the positions array by the hashCode.
+     *
+     * @param hashCode the hashCode
+     * @return the start index in the positions array to search for the key
+     */
+    protected final int calcStartIndexByHashCode(final int hashCode) {
+        return hashCode & (positions.length - 1);
+    }
+
+    /**
+     * Calculates the new size of the positions array, if it needs to be grown.
+     *
+     * @return the new size or -1 if it does not need to be grown
+     */
+    private int calcNewPositionsSize() {
+        if (keysPos << 1 > positions.length) { /*grow*/
+            final var newLength = positions.length << 1;
+            return newLength < 0 ? Integer.MAX_VALUE : newLength;
+        }
+        return -1;
+    }
+
+    /**
+     * Grows the positions array if needed.
+     */
+    protected final void growPositionsArrayIfNeeded() {
+        final var newSize = calcNewPositionsSize();
+        if (newSize < 0) {
+            return;
+        }
+        final var oldPositions = this.positions;
+        this.positions = new int[newSize];
+        for (int oldPosition : oldPositions) {
+            if (0 != oldPosition) {
+                this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition;
+            }
+        }
+    }
+
+    /**
+     * Grow the positions array if needed.
+     *
+     * @return true if the positions array was grown
+     */
+    protected final boolean tryGrowPositionsArrayIfNeeded() {
+        final var newSize = calcNewPositionsSize();
+        if (newSize < 0) {
+            return false;
+        }
+        final var oldPositions = this.positions;
+        this.positions = new int[newSize];
+        for (int oldPosition : oldPositions) {
+            if (0 != oldPosition) {
+                this.positions[findEmptySlotWithoutEqualityCheck(hashCodesOrDeletedIndices[~oldPosition])] = oldPosition;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the number of elements in this collection.  If this collection
+     * contains more than {@code Integer.MAX_VALUE} elements, returns
+     * {@code Integer.MAX_VALUE}.
+     *
+     * @return the number of elements in this collection
+     */
+    @Override
+    public int size() {
+        return keysPos - removedKeysCount;
+    }
+
+    /**
+     * Finds the next free slot in the keys array.
+     * If the keys array needs to be grown, it is grown.
+     * If there are deleted keys, the index of the last deleted key is returned.
+     *
+     * @return the index of the next free slot
+     */
+    protected final int getFreeKeyIndex() {
+        final int index;
+        if (lastDeletedIndex == -1) {
+            index = keysPos++;
+            if (index == keys.length) {
+                growKeysAndHashCodeArrays();
+            }
+        } else {
+            index = lastDeletedIndex;
+            lastDeletedIndex = hashCodesOrDeletedIndices[lastDeletedIndex];
+            removedKeysCount--;
+        }
+        return index;
+    }
+
+    /**
+     * Grow the keys and hashCodes arrays.
+     */
+    protected void growKeysAndHashCodeArrays() {
+        var newSize = (keys.length >> 1) + keys.length;
+        if (newSize < 0) {
+            newSize = Integer.MAX_VALUE;
+        }
+        final var oldKeys = this.keys;
+        this.keys = newKeysArray(newSize);
+        System.arraycopy(oldKeys, 0, keys, 0, oldKeys.length);
+        final var oldHashCodes = this.hashCodesOrDeletedIndices;
+        this.hashCodesOrDeletedIndices = new int[newSize];
+        System.arraycopy(oldHashCodes, 0, hashCodesOrDeletedIndices, 0, oldHashCodes.length);
+    }
+
+    @Override
+    public final boolean tryRemove(K o) {
+        return tryRemove(o, o.hashCode());
+    }
+
+    public final boolean tryRemove(K e, int hashCode) {
+        final var index = findPosition(e, hashCode);
+        if (index < 0) {
+            return false;
+        }
+        removeFrom(index);
+        return true;
+    }
+
+    /**
+     * Removes the element at the given position.
+     *
+     * @param e the element
+     * @return the index of the removed element or -1 if the element was not found
+     */
+    public final int removeAndGetIndex(final K e) {
+        return removeAndGetIndex(e, e.hashCode());
+    }
+
+    /**
+     * Removes the element at the given position.
+     *
+     * @param e        the element
+     * @param hashCode the hash code of the element. This is a performance optimization.
+     * @return the index of the removed element or -1 if the element was not found
+     */
+    public final int removeAndGetIndex(final K e, final int hashCode) {
+        final var pIndex = findPosition(e, hashCode);
+        if (pIndex < 0) {
+            return -1;
+        }
+        final var eIndex = ~positions[pIndex];
+        removeFrom(pIndex);
+        return eIndex;
+    }
+
+    @Override
+    public final void removeUnchecked(K e) {
+        removeUnchecked(e, e.hashCode());
+    }
+
+    public final void removeUnchecked(K e, int hashCode) {
+        removeFrom(findPosition(e, hashCode));
+    }
+
+    /**
+     * Removes the element at the given position.
+     * <p>
+     * This is an implementation of Knuth's Algorithm R from tAoCP vol3, p 527,
+     * with exchanging of the roles of i and j so that they can be usefully renamed
+     * to <i>here</i> and <i>scan</i>.
+     * <p>
+     * It relies on linear probing but doesn't require a distinguished REMOVED
+     * value. Since we resize the table when it gets fullish, we don't worry [much]
+     * about the overhead of the linear probing.
+     * <p>
+     *
+     * @param here the index in the positions array
+     */
+    protected void removeFrom(int here) {
+        final var pIndex = ~positions[here];
+        hashCodesOrDeletedIndices[pIndex] = lastDeletedIndex;
+        lastDeletedIndex = pIndex;
+        removedKeysCount++;
+        keys[pIndex] = null;
+        while (true) {
+            positions[here] = 0;
+            int scan = here;
+            while (true) {
+                if (--scan < 0) scan += positions.length;
+                if (positions[scan] == 0) return;
+                int r = calcStartIndexByHashCode(hashCodesOrDeletedIndices[~positions[scan]]);
+                if ((scan > r || r >= here) && (r >= here || here >= scan) && (here >= scan || scan > r)) {
+                    positions[here] = positions[scan];
+                    here = scan;
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if this collection contains no elements.
+     *
+     * @return {@code true} if this collection contains no elements
+     */
+    @Override
+    public final boolean isEmpty() {
+        return this.size() == 0;
+    }
+
+    @Override
+    public final boolean containsKey(K o) {
+        final int hashCode = o.hashCode();
+        var pIndex = calcStartIndexByHashCode(hashCode);
+        while (true) {
+            if (0 == positions[pIndex]) {
+                return false;
+            } else {
+                final var eIndex = ~positions[pIndex];
+                if (hashCode == hashCodesOrDeletedIndices[eIndex] && o.equals(keys[eIndex])) {
+                    return true;
+                } else if (--pIndex < 0) {
+                    pIndex += positions.length;
+                }
+            }
+        }
+    }
+
+    /**
+     * Attentions: Due to the ordering of the keys, this method may be slow
+     * if matching elements are at the start of the list.
+     * Try to use {@link #anyMatchRandomOrder(Predicate)} instead.
+     */
+    @Override
+    public final boolean anyMatch(Predicate<K> predicate) {
+        var pos = keysPos - 1;
+        while (-1 < pos) {
+            if (null != keys[pos] && predicate.test(keys[pos])) {
+                return true;
+            }
+            pos--;
+        }
+        return false;
+    }
+
+    /**
+     * This method can be faster than {@link #anyMatch(Predicate)} if one expects
+     * to find many matches. But it is slower if one expects to find no matches or just a single one.
+     *
+     * @param predicate the predicate to apply to elements of this collection
+     * @return {@code true} if any element of the collection matches the predicate
+     */
+    public final boolean anyMatchRandomOrder(Predicate<K> predicate) {
+        var pIndex = positions.length - 1;
+        while (-1 < pIndex) {
+            if (0 != positions[pIndex] && predicate.test(keys[~positions[pIndex]])) {
+                return true;
+            }
+            pIndex--;
+        }
+        return false;
+    }
+
+    @Override
+    public final ExtendedIterator<K> keyIterator() {
+        final var initialSize = size();
+        final Runnable checkForConcurrentModification = () ->
+        {
+            if (size() != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArrayIterator<>(keys, keysPos, checkForConcurrentModification);
+    }
+
+    protected final int findPosition(final K e, final int hashCode) {
+        var pIndex = calcStartIndexByHashCode(hashCode);
+        while (true) {
+            if (0 == positions[pIndex]) {
+                return ~pIndex;
+            } else {
+                final var pos = ~positions[pIndex];
+                if (hashCode == hashCodesOrDeletedIndices[pos] && e.equals(keys[pos])) {
+                    return pIndex;
+                } else if (--pIndex < 0) {
+                    pIndex += positions.length;
+                }
+            }
+        }
+    }
+
+    protected final int findEmptySlotWithoutEqualityCheck(final int hashCode) {
+        var pIndex = calcStartIndexByHashCode(hashCode);
+        while (true) {
+            if (0 == positions[pIndex]) {
+                return pIndex;
+            } else if (--pIndex < 0) {
+                pIndex += positions.length;
+            }
+        }
+    }
+
+    /**
+     * Removes all the elements from this collection (optional operation).
+     * The collection will be empty after this method returns.
+     *
+     * @throws UnsupportedOperationException if the {@code clear} operation
+     *                                       is not supported by this collection
+     */
+    @Override
+    public void clear() {
+        positions = new int[MINIMUM_HASHES_SIZE];
+        keys = newKeysArray(MINIMUM_ELEMENTS_SIZE);
+        hashCodesOrDeletedIndices = new int[MINIMUM_ELEMENTS_SIZE];
+        keysPos = 0;
+        lastDeletedIndex = -1;
+        removedKeysCount = 0;
+    }
+
+    @Override
+    public final Spliterator<K> keySpliterator() {
+        final var initialSize = this.size();
+        final Runnable checkForConcurrentModification = () ->
+        {
+            if (this.size() != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArraySpliterator<>(keys, keysPos, checkForConcurrentModification);
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashMap.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashMap.java
new file mode 100644
index 0000000000..b9a162908b
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashMap.java
@@ -0,0 +1,208 @@
+/*
+ * 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.jena.mem2.collection;
+
+import org.apache.jena.mem2.iterator.SparseArrayIterator;
+import org.apache.jena.mem2.spliterator.SparseArraySpliterator;
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.ConcurrentModificationException;
+import java.util.Spliterator;
+import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
+
+/**
+ * Map which grows, if needed but never shrinks.
+ * This map does not guarantee any order. Although due to the way it is implemented the elements have a certain order.
+ * This map does not allow null keys.
+ * This map is not thread safe.
+ * It´s purpose is to support fast add, remove, contains and stream / iterate operations.
+ * Only remove operations are not as fast as in {@link java.util.HashMap}
+ * Iterating over this map does not get much faster again after removing elements because the map is not compacted.
+ */
+public abstract class FastHashMap<K, V> extends FastHashBase<K> implements JenaMap<K, V> {
+
+    protected V[] values;
+
+    protected FastHashMap(int initialSize) {
+        super(initialSize);
+        this.values = newValuesArray(keys.length);
+    }
+
+    protected FastHashMap() {
+        super();
+        this.values = newValuesArray(keys.length);
+    }
+
+    protected abstract V[] newValuesArray(int size);
+
+    @Override
+    protected void growKeysAndHashCodeArrays() {
+        super.growKeysAndHashCodeArrays();
+        final var oldValues = values;
+        values = newValuesArray(keys.length);
+        System.arraycopy(oldValues, 0, values, 0, oldValues.length);
+    }
+
+    @Override
+    protected void removeFrom(int here) {
+        values[~positions[here]] = null;
+        super.removeFrom(here);
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        values = newValuesArray(keys.length);
+    }
+
+    @Override
+    public boolean tryPut(K key, V value) {
+        final var hashCode = key.hashCode();
+        var pIndex = findPosition(key, hashCode);
+        if (pIndex < 0) {
+            if (tryGrowPositionsArrayIfNeeded()) {
+                pIndex = findPosition(key, hashCode);
+            }
+            final var eIndex = getFreeKeyIndex();
+            keys[eIndex] = key;
+            values[eIndex] = value;
+            hashCodesOrDeletedIndices[eIndex] = hashCode;
+            positions[~pIndex] = ~eIndex;
+            return true;
+        } else {
+            values[~positions[pIndex]] = value;
+            return false;
+        }
+    }
+
+    @Override
+    public void put(K key, V value) {
+        final var hashCode = key.hashCode();
+        var pIndex = findPosition(key, hashCode);
+        if (pIndex < 0) {
+            if (tryGrowPositionsArrayIfNeeded()) {
+                pIndex = findPosition(key, hashCode);
+            }
+            final var eIndex = getFreeKeyIndex();
+            keys[eIndex] = key;
+            values[eIndex] = value;
+            hashCodesOrDeletedIndices[eIndex] = hashCode;
+            positions[~pIndex] = ~eIndex;
+        } else {
+            values[~positions[pIndex]] = value;
+        }
+    }
+
+    /**
+     * Returns the value at the given index.
+     *
+     * @param i index
+     * @return value
+     */
+    public V getValueAt(int i) {
+        return values[i];
+    }
+
+    @Override
+    public V get(K key) {
+        var pIndex = findPosition(key, key.hashCode());
+        if (pIndex < 0) {
+            return null;
+        } else {
+            return values[~positions[pIndex]];
+        }
+    }
+
+    @Override
+    public V getOrDefault(K key, V defaultValue) {
+        var pIndex = findPosition(key, key.hashCode());
+        if (pIndex < 0) {
+            return defaultValue;
+        } else {
+            return values[~positions[pIndex]];
+        }
+    }
+
+    @Override
+    public V computeIfAbsent(K key, Supplier<V> absentValueSupplier) {
+        final var hashCode = key.hashCode();
+        var pIndex = findPosition(key, hashCode);
+        if (pIndex < 0) {
+            if (tryGrowPositionsArrayIfNeeded()) {
+                pIndex = findPosition(key, hashCode);
+            }
+            final var eIndex = getFreeKeyIndex();
+            keys[eIndex] = key;
+            hashCodesOrDeletedIndices[eIndex] = hashCode;
+            final var value = absentValueSupplier.get();
+            values[eIndex] = value;
+            positions[~pIndex] = ~eIndex;
+            return value;
+        } else {
+            return values[~positions[pIndex]];
+        }
+    }
+
+    @Override
+    public void compute(K key, UnaryOperator<V> valueProcessor) {
+        final int hashCode = key.hashCode();
+        var pIndex = findPosition(key, hashCode);
+        if (pIndex < 0) {
+            final var value = valueProcessor.apply(null);
+            if (value == null)
+                return;
+            final var eIndex = getFreeKeyIndex();
+            keys[eIndex] = key;
+            hashCodesOrDeletedIndices[eIndex] = hashCode;
+            values[eIndex] = value;
+            positions[~pIndex] = ~eIndex;
+            tryGrowPositionsArrayIfNeeded();
+        } else {
+            var eIndex = ~positions[pIndex];
+            final var value = valueProcessor.apply(values[eIndex]);
+            if (value == null) {
+                removeFrom(pIndex);
+            } else {
+                values[eIndex] = value;
+            }
+        }
+    }
+
+
+    @Override
+    public ExtendedIterator<V> valueIterator() {
+        final var initialSize = size();
+        final Runnable checkForConcurrentModification = () ->
+        {
+            if (size() != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArrayIterator<>(values, keysPos, checkForConcurrentModification);
+    }
+
+    @Override
+    public Spliterator<V> valueSpliterator() {
+        final var initialSize = this.size();
+        final Runnable checkForConcurrentModification = () ->
+        {
+            if (this.size() != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArraySpliterator<>(values, keysPos, checkForConcurrentModification);
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashSet.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashSet.java
new file mode 100644
index 0000000000..1a9d15bc07
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/FastHashSet.java
@@ -0,0 +1,113 @@
+/*
+ * 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.jena.mem2.collection;
+
+/**
+ * Set which grows, if needed but never shrinks.
+ * This set does not guarantee any order. Although due to the way it is implemented the elements have a certain order.
+ * This set does not allow null values.
+ * This set is not thread safe.
+ * It´s purpose is to support fast add, remove, contains and stream / iterate operations.
+ * Only remove operations are not as fast as in {@link java.util.HashSet}
+ * Iterating over this set not get much faster again after removing elements because the set is not compacted.
+ */
+public abstract class FastHashSet<K> extends FastHashBase<K> implements JenaSetHashOptimized<K> {
+
+    protected FastHashSet(int initialSize) {
+        super(initialSize);
+    }
+
+    protected FastHashSet() {
+        super();
+    }
+
+    @Override
+    public boolean tryAdd(K key) {
+        return tryAdd(key, key.hashCode());
+    }
+
+    @Override
+    public boolean tryAdd(K value, int hashCode) {
+        growPositionsArrayIfNeeded();
+        var pIndex = findPosition(value, hashCode);
+        if (pIndex < 0) {
+            final var eIndex = getFreeKeyIndex();
+            keys[eIndex] = value;
+            hashCodesOrDeletedIndices[eIndex] = hashCode;
+            positions[~pIndex] = ~eIndex;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Add and get the index of the added element.
+     *
+     * @param value the value to add
+     * @return the index of the added element or the inverse (~) index of the existing element
+     */
+    public int addAndGetIndex(K value) {
+        return addAndGetIndex(value, value.hashCode());
+    }
+
+    /**
+     * Add and get the index of the added element.
+     *
+     * @param value    the value to add
+     * @param hashCode the hash code of the value. This is a performance optimization.
+     * @return the index of the added element or the inverse (~) index of the existing element
+     */
+    public int addAndGetIndex(final K value, final int hashCode) {
+        growPositionsArrayIfNeeded();
+        final var pIndex = findPosition(value, hashCode);
+        if (pIndex < 0) {
+            final var eIndex = getFreeKeyIndex();
+            keys[eIndex] = value;
+            hashCodesOrDeletedIndices[eIndex] = hashCode;
+            positions[~pIndex] = ~eIndex;
+            return eIndex;
+        } else {
+            return positions[pIndex];
+        }
+    }
+
+    @Override
+    public void addUnchecked(K key) {
+        addUnchecked(key, key.hashCode());
+    }
+
+    @Override
+    public void addUnchecked(K value, int hashCode) {
+        growPositionsArrayIfNeeded();
+        final var eIndex = getFreeKeyIndex();
+        keys[eIndex] = value;
+        hashCodesOrDeletedIndices[eIndex] = hashCode;
+        positions[findEmptySlotWithoutEqualityCheck(hashCode)] = ~eIndex;
+    }
+
+    /**
+     * Gets the key at the given index.
+     *
+     * @param i the index
+     * @return the key at the given index
+     */
+    public K getKeyAt(int i) {
+        return keys[i];
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonBase.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonBase.java
new file mode 100644
index 0000000000..06d98eca0b
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonBase.java
@@ -0,0 +1,210 @@
+/*
+ * 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.jena.mem2.collection;
+
+import org.apache.jena.mem2.iterator.SparseArrayIterator;
+import org.apache.jena.mem2.spliterator.SparseArraySpliterator;
+import org.apache.jena.shared.JenaException;
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.ConcurrentModificationException;
+import java.util.Spliterator;
+import java.util.function.Predicate;
+
+/**
+ * Common code for hash tables and sets.
+ * The hash table is a lookup table.
+ * The hash codes are not stored. When the table grows, all hash codes need to be recalculated.
+ *
+ * @param <E> the element type
+ */
+public abstract class HashCommonBase<E> {
+    /**
+     * Jeremy suggests, from his experiments, that load factors more than
+     * 0.6 leave the table too dense, and little advantage is gained below 0.4.
+     * Although that was with a quadratic probe, I'm borrowing the same
+     * plausible range, and use 0.5 by default.
+     */
+    protected static final double LOAD_FACTOR = 0.5;
+    // Hash tables are 0.25 to 0.5 full so these numbers
+    // are for storing about 1/3 of that number of items.
+    // The larger sizes are added so that the system has "soft failure"
+    // rather implying guaranteed performance.
+    // https://primes.utm.edu/lists/small/millions/
+    static final int[] primes = {7, 19, 37, 79, 149, 307, 617, 1237, 2477, 4957, 9923, 19_853, 39_709, 79_423, 158_849,
+            317_701, 635_413, 1_270_849, 2_541_701, 5_083_423, 10_166_857, 20_333_759, 40_667_527, 81_335_047,
+            162_670_111, 325_340_233, 650_680_469, 982_451_653 // 50 millionth prime - Largest at primes.utm.edu.
+    };
+    /**
+     * The keys of whatever table it is we're implementing. Since we share code
+     * for triple sets and for node->bunch maps, it has to be an Object array; we
+     * take the casting hit.
+     */
+    protected E[] keys;
+    /**
+     * The threshold number of elements above which we resize the table;
+     * equal to the capacity times the load factor.
+     */
+    protected int threshold;
+    /**
+     * The number of active elements in the table, maintained incrementally.
+     */
+    protected int size = 0;
+
+    protected HashCommonBase(int initialCapacity) {
+        keys = newKeysArray(initialCapacity);
+        threshold = (int) (keys.length * LOAD_FACTOR);
+    }
+
+    protected static int nextSize(int atLeast) {
+        for (int prime : primes) {
+            if (prime > atLeast) return prime;
+        }
+        //return atLeast ;        // Input is 2*current capacity.
+        // There are some very large numbers in the primes table.
+        throw new JenaException("Failed to find a 'next size': atLeast = " + atLeast);
+    }
+
+    protected void clear(int initialCapacity) {
+        size = 0;
+        keys = newKeysArray(initialCapacity);
+        threshold = (int) (keys.length * LOAD_FACTOR);
+    }
+
+    public int size() {
+        return size;
+    }
+
+    public boolean isEmpty() {
+        return size == 0;
+    }
+
+
+    /**
+     * Subclasses must implement to answer a new Key[size] array.
+     */
+    protected abstract E[] newKeysArray(int size);
+
+    /**
+     * Answer the initial index for the object <code>key</code> in the table.
+     * With luck, this will be the final position for that object. The initial index
+     * will always be non-negative and less than <code>capacity</code>.
+     * <p>
+     * Implementation note: do <i>not</i> use <code>Math.abs</code> to turn a
+     * hashcode into a positive value; there is a single specific integer on which
+     * it does not work. (Hence, here, the use of bitmasks.)
+     */
+    protected final int initialIndexFor(int hashOfKey) {
+        return (improveHashCode(hashOfKey) & 0x7fffffff) % keys.length;
+    }
+
+    /**
+     * Answer the transformed hash code, intended to be an improvement
+     * on the objects own hashcode. The magic number 127 is performance
+     * voodoo to (try to) eliminate problems experienced by Wolfgang.
+     */
+    protected int improveHashCode(int hashCode) {
+        return hashCode * 127;
+    }
+
+    protected abstract void grow();
+
+    /**
+     * Work out the capacity and threshold sizes for a new improved bigger
+     * table (bigger by a factor of two, at present).
+     */
+    protected int calcGrownCapacityAndSetThreshold() {
+        final var capacity = HashCommonBase.nextSize(keys.length * 2);
+        threshold = (int) (capacity * LOAD_FACTOR);
+        return capacity;
+    }
+
+    protected abstract void removeFrom(int here);
+
+    /**
+     * Remove the object <code>key</code> from this hash's keys if it
+     * is present (if it's absent, do nothing).
+     */
+    public boolean tryRemove(final E key) {
+        int slot = findSlot(key);
+        if (slot < 0) {
+            removeFrom(~slot);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Remove the object <code>key</code> from this hash's keys if it
+     * is present (if it's absent, do nothing).
+     */
+    public void removeUnchecked(final E key) {
+        int slot = findSlot(key);
+        if (slot < 0) {
+            removeFrom(~slot);
+        }
+    }
+
+    /**
+     * Search for the slot in which <code>key</code> is found. If it is absent,
+     * return the index of the free slot in which it could be placed. If it is present,
+     * return the bitwise complement of the index of the slot it appears in. Hence,
+     * negative values imply present, positive absent, and there's no confusion
+     * around 0.
+     */
+    protected int findSlot(E key) {
+        int index = initialIndexFor(key.hashCode());
+        while (true) {
+            E current = keys[index];
+            if (current == null) return index;
+            if (key.equals(current)) return ~index;
+            if (--index < 0) index += keys.length;
+        }
+    }
+
+    public boolean containsKey(final E key) {
+        return findSlot(key) < 0;
+    }
+
+    public boolean anyMatch(final Predicate<E> predicate) {
+        var pos = keys.length - 1;
+        while (-1 < pos) {
+            if (null != keys[pos] && predicate.test(keys[pos])) {
+                return true;
+            }
+            pos--;
+        }
+        return false;
+    }
+
+    public ExtendedIterator<E> keyIterator() {
+        final var initialSize = size;
+        final Runnable checkForConcurrentModification = () -> {
+            if (size != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArrayIterator<>(keys, checkForConcurrentModification);
+    }
+
+    public Spliterator<E> keySpliterator() {
+        final var initialSize = size;
+        final Runnable checkForConcurrentModification = () -> {
+            if (size != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArraySpliterator<>(keys, checkForConcurrentModification);
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonMap.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonMap.java
new file mode 100644
index 0000000000..52ac143561
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonMap.java
@@ -0,0 +1,193 @@
+/*
+ * 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.jena.mem2.collection;
+
+import org.apache.jena.mem2.iterator.SparseArrayIterator;
+import org.apache.jena.mem2.spliterator.SparseArraySpliterator;
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.ConcurrentModificationException;
+import java.util.Spliterator;
+import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
+
+/**
+ * Implementation of {@link JenaMap} based on {@link HashCommonBase}.
+ */
+public abstract class HashCommonMap<K, V> extends HashCommonBase<K> implements JenaMap<K, V> {
+
+    protected V[] values;
+
+    /**
+     * Initialise this hashed thingy to have <code>initialCapacity</code> as its
+     * capacity and the corresponding threshold. All the key elements start out
+     * null.
+     */
+    protected HashCommonMap(int initialCapacity) {
+        super(initialCapacity);
+        this.values = newValuesArray(keys.length);
+    }
+
+    @Override
+    public void clear(int initialCapacity) {
+        super.clear(initialCapacity);
+        this.values = newValuesArray(keys.length);
+    }
+
+    protected abstract V[] newValuesArray(int size);
+
+    @Override
+    public boolean tryPut(K key, V value) {
+        final var slot = findSlot(key);
+        if (slot < 0) {
+            values[~slot] = value;
+            return false;
+        }
+        keys[slot] = key;
+        values[slot] = value;
+        if (++size > threshold) grow();
+        return true;
+    }
+
+    @Override
+    public void put(K key, V value) {
+        final var slot = findSlot(key);
+        if (slot < 0) {
+            values[~slot] = value;
+            return;
+        }
+        keys[slot] = key;
+        values[slot] = value;
+        if (++size > threshold) grow();
+    }
+
+    @Override
+    public V get(K key) {
+        final var slot = findSlot(key);
+        if (slot < 0) return values[~slot];
+        return null;
+    }
+
+    @Override
+    public V getOrDefault(K key, V defaultValue) {
+        final var slot = findSlot(key);
+        if (slot < 0) return values[~slot];
+        return defaultValue;
+    }
+
+    @Override
+    public V computeIfAbsent(K key, Supplier<V> absentValueSupplier) {
+        final var slot = findSlot(key);
+        if (slot < 0) return values[~slot];
+        final var value = absentValueSupplier.get();
+        keys[slot] = key;
+        values[slot] = value;
+        if (++size > threshold) grow();
+        return value;
+    }
+
+    @Override
+    public void compute(K key, UnaryOperator<V> valueProcessor) {
+        final var slot = findSlot(key);
+        if (slot < 0) {
+            final var value = valueProcessor.apply(values[~slot]);
+            if (value == null) {
+                removeFrom(~slot);
+            } else {
+                values[~slot] = value;
+            }
+        } else {
+            final var value = valueProcessor.apply(null);
+            if (value == null)
+                return;
+            keys[slot] = key;
+            values[slot] = value;
+            if (++size > threshold) grow();
+        }
+    }
+
+
+    @Override
+    protected void grow() {
+        final K[] oldContents = keys;
+        final V[] oldValues = values;
+        keys = newKeysArray(calcGrownCapacityAndSetThreshold());
+        values = newValuesArray(keys.length);
+        for (int i = 0; i < oldContents.length; i += 1) {
+            final K key = oldContents[i];
+            if (key != null) {
+                final int slot = findSlot(key);
+                keys[slot] = key;
+                values[slot] = oldValues[i];
+            }
+        }
+    }
+
+    /**
+     * Remove the triple at element <code>i</code> of <code>contents</code>.
+     * This is an implementation of Knuth's Algorithm R from tAoCP vol3, p 527,
+     * with exchanging of the roles of i and j so that they can be usefully renamed
+     * to <i>here</i> and <i>scan</i>.
+     * <p>
+     * It relies on linear probing but doesn't require a distinguished REMOVED
+     * value. Since we resize the table when it gets fullish, we don't worry [much]
+     * about the overhead of the linear probing.
+     * <p>
+     * Iterators running over the keys may miss elements that are moved from the
+     * bottom of the table to the top because of Iterator::remove. removeFrom
+     * returns such a moved key as its result, and null otherwise.
+     */
+    @Override
+    protected void removeFrom(int here) {
+        size -= 1;
+        while (true) {
+            keys[here] = null;
+            values[here] = null;
+            int scan = here;
+            while (true) {
+                if (--scan < 0) scan += keys.length;
+                if (keys[scan] == null) return;
+                final int r = initialIndexFor(keys[scan].hashCode());
+                if ((scan > r || r >= here) && (r >= here || here >= scan) && (here >= scan || scan > r)) {
+                    keys[here] = keys[scan];
+                    values[here] = values[scan];
+                    here = scan;
+                    break;
+                }
+            }
+        }
+    }
+
+    @Override
+    public ExtendedIterator<V> valueIterator() {
+        final var initialSize = size;
+        final Runnable checkForConcurrentModification = () -> {
+            if (size != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArrayIterator<>(values, checkForConcurrentModification);
+    }
+
+    @Override
+    public Spliterator<V> valueSpliterator() {
+        final var initialSize = size;
+        final Runnable checkForConcurrentModification = () -> {
+            if (size != initialSize) throw new ConcurrentModificationException();
+        };
+        return new SparseArraySpliterator<>(values, checkForConcurrentModification);
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonSet.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonSet.java
new file mode 100644
index 0000000000..5abbd60dcf
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/HashCommonSet.java
@@ -0,0 +1,95 @@
+/*
+ * 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.jena.mem2.collection;
+
+/**
+ * Implementation of {@link JenaSet} based on {@link HashCommonBase}.
+ */
+public abstract class HashCommonSet<K> extends HashCommonBase<K> implements JenaSet<K> {
+
+    /**
+     * Initialise this hashed thingy to have <code>initialCapacity</code> as its
+     * capacity and the corresponding threshold. All the key elements start out
+     * null.
+     */
+    protected HashCommonSet(int initialCapacity) {
+        super(initialCapacity);
+    }
+
+    @Override
+    public boolean tryAdd(K key) {
+        final var slot = findSlot(key);
+        if (slot < 0) return false;
+        keys[slot] = key;
+        if (++size > threshold) grow();
+        return true;
+    }
+
+    @Override
+    public void addUnchecked(K key) {
+        final var slot = findSlot(key);
+        if (slot < 0) return;
+        keys[slot] = key;
+        if (++size > threshold) grow();
+    }
+
+    @Override
+    protected void grow() {
+        final K[] oldContents = keys;
+        keys = newKeysArray(calcGrownCapacityAndSetThreshold());
+        for (final K key : oldContents) {
+            if (key != null) {
+                final int slot = findSlot(key);
+                keys[slot] = key;
+            }
+        }
+    }
+
+    /**
+     * Remove the triple at element <code>i</code> of <code>contents</code>.
+     * This is an implementation of Knuth's Algorithm R from tAoCP vol3, p 527,
+     * with exchanging of the roles of i and j so that they can be usefully renamed
+     * to <i>here</i> and <i>scan</i>.
+     * <p>
+     * It relies on linear probing but doesn't require a distinguished REMOVED
+     * value. Since we resize the table when it gets fullish, we don't worry [much]
+     * about the overhead of the linear probing.
+     * <p>
+     * Iterators running over the keys may miss elements that are moved from the
+     * bottom of the table to the top because of Iterator::remove. removeFrom
+     * returns such a moved key as its result, and null otherwise.
+     */
+    @Override
+    protected void removeFrom(int here) {
+        size -= 1;
+        while (true) {
+            keys[here] = null;
+            int scan = here;
+            while (true) {
+                if (--scan < 0) scan += keys.length;
+                if (keys[scan] == null) return;
+                final int r = initialIndexFor(keys[scan].hashCode());
+                if ((scan > r || r >= here) && (r >= here || here >= scan) && (here >= scan || scan > r)) {
+                    keys[here] = keys[scan];
+                    here = scan;
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaMap.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaMap.java
new file mode 100644
index 0000000000..7b226785ec
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaMap.java
@@ -0,0 +1,121 @@
+/*
+ * 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.jena.mem2.collection;
+
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.Spliterator;
+import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * A map from keys of type {@code K} to values of type {@code V}.
+ *
+ * @param <K> the type of the keys in the map
+ * @param <V> the type of the values in the map
+ */
+public interface JenaMap<K, V> extends JenaMapSetCommon<K> {
+
+    /**
+     * Try to put a key-value pair into the map. If the key is already present, the value is updated.
+     *
+     * @param key   the key to put
+     * @param value the value to put
+     * @return true if the key-value pair was put into the map, false if the key was already present
+     */
+    boolean tryPut(K key, V value);
+
+    /**
+     * Put a key-value pair into the map. If the key is already present, the value is updated.
+     *
+     * @param key   the key to put
+     * @param value the value to put
+     */
+    void put(K key, V value);
+
+    /**
+     * Get the value associated with the provided key.
+     *
+     * @param key the key to look up
+     * @return the value associated with the key, or null if the key is not present
+     */
+    V get(K key);
+
+    /**
+     * Get the value associated with the provided key, or a default value if the key is not present.
+     *
+     * @param key          the key to look up
+     * @param defaultValue the default value to return if the key is not present
+     * @return the value associated with the key, or the default value if the key is not present
+     */
+    V getOrDefault(K key, V defaultValue);
+
+    /**
+     * Compute a value for a key if the key is not present.
+     * The value is automatically put into the map.
+     *
+     * @param key                 the key whose value is to retrieved or computed
+     * @param absentValueSupplier the function to compute a value for the key, if the key is not present
+     * @return the value associated with the key, or the value computed by the function if the key is not present
+     */
+    V computeIfAbsent(K key, Supplier<V> absentValueSupplier);
+
+    /**
+     * Compute a value for a key.
+     *
+     * @param key            the key to compute a value for
+     * @param valueProcessor the function to compute a value for the key. The function is passed the current value
+     *                       associated with the key, or null if the key is not present. The function should return
+     *                       the new value to associate with the key, or null if the key should be removed from the map.
+     */
+    void compute(K key, UnaryOperator<V> valueProcessor);
+
+    /**
+     * Get an iterator over the values in the map.
+     *
+     * @return an iterator over the values in the map
+     */
+    ExtendedIterator<V> valueIterator();
+
+    /**
+     * Get a spliterator over the values in the map.
+     *
+     * @return a spliterator over the values in the map
+     */
+    Spliterator<V> valueSpliterator();
+
+    /**
+     * Get a stream over the values in the map.
+     *
+     * @return a stream over the values in the map
+     */
+    default Stream<V> valueStream() {
+        return StreamSupport.stream(valueSpliterator(), false);
+    }
+
+    /**
+     * Get a parallel stream over the values in the map.
+     *
+     * @return a parallel stream over the values in the map
+     */
+    default Stream<V> valueStreamParallel() {
+        return StreamSupport.stream(valueSpliterator(), false);
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaMapSetCommon.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaMapSetCommon.java
new file mode 100644
index 0000000000..405c94cf1d
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaMapSetCommon.java
@@ -0,0 +1,113 @@
+/*
+ * 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.jena.mem2.collection;
+
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.Spliterator;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Common interface for {@link JenaMap} and {@link JenaSet}. *
+ *
+ * @param <E> the type of the keys/elements in the collection
+ */
+public interface JenaMapSetCommon<E> {
+
+    /**
+     * Clear the collection.
+     */
+    void clear();
+
+    /**
+     * @return the number of elements in the collection
+     */
+    int size();
+
+    /**
+     * @return true if the collection is empty
+     */
+    boolean isEmpty();
+
+    /**
+     * Check whether the collection contains a given key.
+     *
+     * @param key the key to look for
+     * @return true if the collection contains the key
+     */
+    boolean containsKey(E key);
+
+    /**
+     * Check whether the collection contains any element matching the predicate.
+     *
+     * @param predicate the predicate to match
+     * @return true if the collection contains any element matching the predicate
+     */
+    boolean anyMatch(Predicate<E> predicate);
+
+    /**
+     * Tries to remove a key from the collection.
+     *
+     * @param key the key to remove
+     * @return true if the key was removed. If the key was not present, false is returned.
+     */
+    boolean tryRemove(E key);
+
+    /**
+     * Removes a key from the collection.
+     * Attention: Implementations may assume that the key is present.
+     *
+     * @param key the key to remove
+     */
+    void removeUnchecked(E key);
+
+    /**
+     * Get an iterator over the keys in the collection.
+     *
+     * @return an iterator over the keys in the collection
+     */
+    ExtendedIterator<E> keyIterator();
+
+    /**
+     * Get a spliterator over the keys in the collection.
+     *
+     * @return a spliterator over the keys in the collection
+     */
+    Spliterator<E> keySpliterator();
+
+    /**
+     * Get a stream over the keys in the collection.
+     *
+     * @return a stream over the keys in the collection
+     */
+    default Stream<E> keyStream() {
+        return StreamSupport.stream(keySpliterator(), false);
+    }
+
+    /**
+     * Get a parallel stream over the keys in the collection.
+     *
+     * @return a parallel stream over the keys in the collection
+     */
+    default Stream<E> keyStreamParallel() {
+        return StreamSupport.stream(keySpliterator(), true);
+    }
+
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaSet.java
similarity index 54%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/main/java/org/apache/jena/mem2/collection/JenaSet.java
index f601955053..a170cee422 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaSet.java
@@ -15,18 +15,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
-
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
+package org.apache.jena.mem2.collection;
 
 /**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
+ * Set interface specialized for the use cases in triple store implementations.
+ *
+ * @param <E>
  */
-public class Releases {
+public interface JenaSet<E> extends JenaMapSetCommon<E> {
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    /**
+     * Add the key to the set if it is not already present.
+     *
+     * @param key the key to add
+     * @return true if the key was added, false if it was already present
+     */
+    boolean tryAdd(E key);
 
+    /**
+     * Add the key to the set without checking if it is already present.
+     * Attention: This method must only be used if it is guaranteed that the key is not already present.
+     *
+     * @param key the key to add
+     */
+    void addUnchecked(E key);
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaSetHashOptimized.java
similarity index 58%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/main/java/org/apache/jena/mem2/collection/JenaSetHashOptimized.java
index f601955053..2400f09305 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/main/java/org/apache/jena/mem2/collection/JenaSetHashOptimized.java
@@ -15,18 +15,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.collection;
 
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
 
 /**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
+ * Extension of {@link JenaSet} that allows to add and remove elements
+ * with a given hash code.
+ * This is useful if the hash code is already known.
+ * Attention: The hash code must be consistent with E::hashCode().
  */
-public class Releases {
+public interface JenaSetHashOptimized<E> extends JenaSet<E> {
+    boolean tryAdd(E key, int hashCode);
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    void addUnchecked(E key, int hashCode);
 
+    boolean tryRemove(E key, int hashCode);
+
+    void removeUnchecked(E key, int hashCode);
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/iterator/IteratorOfJenaSets.java b/jena-core/src/main/java/org/apache/jena/mem2/iterator/IteratorOfJenaSets.java
new file mode 100644
index 0000000000..68ae6c197f
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/iterator/IteratorOfJenaSets.java
@@ -0,0 +1,74 @@
+/*
+ * 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.jena.mem2.iterator;
+
+import org.apache.jena.mem2.collection.JenaSet;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.function.Consumer;
+
+/**
+ * Iterator that iterates over the entries of sets which are contained in the given iterator of sets.
+ *
+ * @param <E> the type of the elements
+ */
+public class IteratorOfJenaSets<E> extends NiceIterator<E> {
+
+    final Iterator<? extends JenaSet<E>> parentIterator;
+
+    ExtendedIterator<E> currentIterator;
+
+    public IteratorOfJenaSets(Iterator<? extends JenaSet<E>> parentIterator) {
+        this.parentIterator = parentIterator;
+        this.currentIterator = parentIterator.hasNext()
+                ? parentIterator.next().keyIterator()
+                : NiceIterator.emptyIterator();
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (this.currentIterator.hasNext()) {
+            return true;
+        }
+        while (this.parentIterator.hasNext()) {
+            this.currentIterator = this.parentIterator.next().keyIterator();
+            if (this.currentIterator.hasNext()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public E next() {
+        if (this.hasNext()) {
+            return this.currentIterator.next();
+        }
+        throw new NoSuchElementException();
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        this.currentIterator.forEachRemaining(action);
+        this.parentIterator.forEachRemaining(i -> i.keyIterator().forEachRemaining(action));
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/iterator/SparseArrayIterator.java b/jena-core/src/main/java/org/apache/jena/mem2/iterator/SparseArrayIterator.java
new file mode 100644
index 0000000000..8e9a3a4593
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/iterator/SparseArrayIterator.java
@@ -0,0 +1,97 @@
+/*
+ * 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.jena.mem2.iterator;
+
+import org.apache.jena.util.iterator.NiceIterator;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.function.Consumer;
+
+/**
+ * An iterator over a sparse array, that skips null entries.
+ *
+ * @param <E> the type of the array elements
+ */
+public class SparseArrayIterator<E> extends NiceIterator<E> implements Iterator<E> {
+
+    private final E[] entries;
+    private final Runnable checkForConcurrentModification;
+    private int pos;
+    private boolean hasNext = false;
+
+    public SparseArrayIterator(final E[] entries, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.pos = entries.length - 1;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+    public SparseArrayIterator(final E[] entries, int toIndexExclusive, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.pos = toIndexExclusive - 1;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+    /**
+     * Returns {@code true} if the iteration has more elements.
+     * (In other words, returns {@code true} if {@link #next} would
+     * return an element rather than throwing an exception.)
+     *
+     * @return {@code true} if the iteration has more elements
+     */
+    @Override
+    public boolean hasNext() {
+        while (-1 < pos) {
+            if (null != entries[pos]) {
+                hasNext = true;
+                return true;
+            }
+            pos--;
+        }
+        hasNext = false;
+        return false;
+    }
+
+    /**
+     * Returns the next element in the iteration.
+     *
+     * @return the next element in the iteration
+     * @throws NoSuchElementException if the iteration has no more elements
+     */
+    @Override
+    public E next() {
+        this.checkForConcurrentModification.run();
+        if (hasNext || hasNext()) {
+            hasNext = false;
+            return entries[pos--];
+        }
+        throw new NoSuchElementException();
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        while (-1 < pos) {
+            if (null != entries[pos]) {
+                action.accept(entries[pos]);
+            }
+            pos--;
+        }
+        this.checkForConcurrentModification.run();
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/pattern/MatchPattern.java b/jena-core/src/main/java/org/apache/jena/mem2/pattern/MatchPattern.java
new file mode 100644
index 0000000000..a0337fb25b
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/pattern/MatchPattern.java
@@ -0,0 +1,58 @@
+/*
+ * 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.jena.mem2.pattern;
+
+/**
+ * A pattern for matching triples.
+ * The pattern is defined by the wildcard positions for the subject, predicate and object.
+ */
+public enum MatchPattern {
+    /**
+     * Match a triple with a concrete subject, predicate and object.
+     */
+    SUB_PRE_OBJ,
+    /**
+     * Match a triple with a concrete subject and predicate, and a wildcard object.
+     */
+    SUB_PRE_ANY,
+    /**
+     * Match a triple with a concrete subject and object, and a wildcard predicate.
+     */
+    SUB_ANY_OBJ,
+    /**
+     * Match a triple with a concrete subject, and wildcard predicate and object.
+     */
+    SUB_ANY_ANY,
+    /**
+     * Match a triple with a concrete predicate and object, and a wildcard subject.
+     */
+    ANY_PRE_OBJ,
+    /**
+     * Match a triple with a concrete predicate, and wildcard subject and object.
+     */
+    ANY_PRE_ANY,
+    /**
+     * Match a triple with a concrete object, and wildcard subject and predicate.
+     */
+    ANY_ANY_OBJ,
+    /**
+     * Match a triple with a wildcard subject, predicate and object.
+     */
+    ANY_ANY_ANY
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/pattern/PatternClassifier.java b/jena-core/src/main/java/org/apache/jena/mem2/pattern/PatternClassifier.java
new file mode 100644
index 0000000000..422821f9a5
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/pattern/PatternClassifier.java
@@ -0,0 +1,105 @@
+/*
+ * 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.jena.mem2.pattern;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+/**
+ * Classify a triple match into one of the 8 match patterns.
+ * <p>
+ * The classification is based on the concrete-ness of the subject, predicate and object.
+ * A concrete node is one that is not a variable.
+ * <p>
+ * The classification is used to select the most efficient implementation of a triple store.
+ * <p>
+ * This is a utility class; there is no need to instantiate it.
+ *
+ * @see MatchPattern
+ */
+public class PatternClassifier {
+
+    private PatternClassifier() {
+    }
+
+    public static MatchPattern classify(Triple tripleMatch) {
+        if (tripleMatch.isConcrete()) {
+            return MatchPattern.SUB_PRE_OBJ;
+        } else {
+            if (tripleMatch.getSubject().isConcrete()) {
+                if (tripleMatch.getPredicate().isConcrete()) {
+                    return MatchPattern.SUB_PRE_ANY;
+                } else {
+                    if (tripleMatch.getObject().isConcrete()) {
+                        return MatchPattern.SUB_ANY_OBJ;
+                    } else {
+                        return MatchPattern.SUB_ANY_ANY;
+                    }
+                }
+            } else {
+                if (tripleMatch.getPredicate().isConcrete()) {
+                    if (tripleMatch.getObject().isConcrete()) {
+                        return MatchPattern.ANY_PRE_OBJ;
+                    } else {
+                        return MatchPattern.ANY_PRE_ANY;
+                    }
+                } else {
+                    if (tripleMatch.getObject().isConcrete()) {
+                        return MatchPattern.ANY_ANY_OBJ;
+                    } else {
+                        return MatchPattern.ANY_ANY_ANY;
+                    }
+                }
+            }
+        }
+    }
+
+    public static MatchPattern classify(Node sm, Node pm, Node om) {
+        if (null != sm && sm.isConcrete()) {
+            if (null != pm && pm.isConcrete()) {
+                if (null != om && om.isConcrete()) {
+                    return MatchPattern.SUB_PRE_OBJ;
+                } else {
+                    return MatchPattern.SUB_PRE_ANY;
+                }
+            } else {
+                if (null != om && om.isConcrete()) {
+                    return MatchPattern.SUB_ANY_OBJ;
+                } else {
+                    return MatchPattern.SUB_ANY_ANY;
+                }
+            }
+        } else {
+            if (null != pm && pm.isConcrete()) {
+                if (null != om && om.isConcrete()) {
+                    return MatchPattern.ANY_PRE_OBJ;
+                } else {
+                    return MatchPattern.ANY_PRE_ANY;
+                }
+            } else {
+                if (null != om && om.isConcrete()) {
+                    return MatchPattern.ANY_ANY_OBJ;
+                } else {
+                    return MatchPattern.ANY_ANY_ANY;
+                }
+            }
+        }
+
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/spliterator/ArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/ArraySpliterator.java
new file mode 100644
index 0000000000..b759e9f283
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/ArraySpliterator.java
@@ -0,0 +1,101 @@
+/*
+ * 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.jena.mem2.spliterator;
+
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+/**
+ * A spliterator for arrays. This spliterator will iterate over the array
+ * entries within the given boundaries.
+ * <p>
+ * This spliterator supports splitting into sub-spliterators.
+ * <p>
+ * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
+ * before each action.
+ *
+ * @param <E>
+ */
+public class ArraySpliterator<E> implements Spliterator<E> {
+
+    private final E[] entries;
+    private final Runnable checkForConcurrentModification;
+    private int pos;
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param toIndex                        the index of the last element, exclusive
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public ArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.pos = toIndex;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public ArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
+        this(entries, entries.length, checkForConcurrentModification);
+    }
+
+    @Override
+    public boolean tryAdvance(Consumer<? super E> action) {
+        this.checkForConcurrentModification.run();
+        if (-1 < --pos) {
+            action.accept(entries[pos]);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        while (-1 < --pos) {
+            action.accept(entries[pos]);
+        }
+        this.checkForConcurrentModification.run();
+    }
+
+    @Override
+    public Spliterator<E> trySplit() {
+        if (pos < 2) {
+            return null;
+        }
+        final int toIndexOfSubIterator = this.pos;
+        this.pos = pos >>> 1;
+        return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+    }
+
+    @Override
+    public long estimateSize() {
+        return pos;
+    }
+
+    @Override
+    public int characteristics() {
+        return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/spliterator/ArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/ArraySubSpliterator.java
new file mode 100644
index 0000000000..2ad449797a
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/ArraySubSpliterator.java
@@ -0,0 +1,105 @@
+/*
+ * 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.jena.mem2.spliterator;
+
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+/**
+ * A spliterator for arrays. This spliterator will iterate over the array
+ * entries within the given boundaries.
+ * <p>
+ * This spliterator supports splitting into sub-spliterators.
+ * <p>
+ * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
+ * before each action.
+ *
+ * @param <E>
+ */
+public class ArraySubSpliterator<E> implements Spliterator<E> {
+
+    private final E[] entries;
+    private final int fromIndex;
+    private final Runnable checkForConcurrentModification;
+    private int pos;
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param fromIndex                      the index of the first element, inclusive
+     * @param toIndex                        the index of the last element, exclusive
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public ArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.fromIndex = fromIndex;
+        this.pos = toIndex;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public ArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
+        this(entries, 0, entries.length, checkForConcurrentModification);
+    }
+
+    @Override
+    public boolean tryAdvance(Consumer<? super E> action) {
+        this.checkForConcurrentModification.run();
+        if (fromIndex <= --pos) {
+            action.accept(entries[pos]);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        while (fromIndex <= --pos) {
+            action.accept(entries[pos]);
+        }
+        this.checkForConcurrentModification.run();
+    }
+
+    @Override
+    public Spliterator<E> trySplit() {
+        final int entriesCount = pos - fromIndex;
+        if (entriesCount < 2) {
+            return null;
+        }
+        final int toIndexOfSubIterator = this.pos;
+        this.pos = fromIndex + (entriesCount >>> 1);
+        return new ArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+    }
+
+    @Override
+    public long estimateSize() {
+        return pos - fromIndex;
+    }
+
+    @Override
+    public int characteristics() {
+        return DISTINCT | SIZED | SUBSIZED | NONNULL | IMMUTABLE;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/spliterator/SparseArraySpliterator.java b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/SparseArraySpliterator.java
new file mode 100644
index 0000000000..23efa96c8f
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/SparseArraySpliterator.java
@@ -0,0 +1,106 @@
+/*
+ * 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.jena.mem2.spliterator;
+
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+/**
+ * A spliterator for sparse arrays. This spliterator will iterate over the array
+ * skipping null entries.
+ * <p>
+ * This spliterator supports splitting into sub-spliterators.
+ * <p>
+ * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
+ * before each action.
+ *
+ * @param <E> the type of the array elements
+ */
+public class SparseArraySpliterator<E> implements Spliterator<E> {
+
+    private final E[] entries;
+    private int pos;
+    private final Runnable checkForConcurrentModification;
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param toIndex                        the index of the last element, exclusive
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public SparseArraySpliterator(final E[] entries, final int toIndex, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.pos = toIndex;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public SparseArraySpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
+        this(entries, entries.length, checkForConcurrentModification);
+    }
+
+
+    @Override
+    public boolean tryAdvance(Consumer<? super E> action) {
+        this.checkForConcurrentModification.run();
+        while (-1 < --pos) {
+            if (null != entries[pos]) {
+                action.accept(entries[pos]);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        pos--;
+        while (-1 < pos) {
+            if (null != entries[pos]) {
+                action.accept(entries[pos]);
+            }
+            pos--;
+        }
+        this.checkForConcurrentModification.run();
+    }
+
+    @Override
+    public Spliterator<E> trySplit() {
+        if (pos < 2) {
+            return null;
+        }
+        final int toIndexOfSubIterator = this.pos;
+        this.pos = pos >>> 1;
+        return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+    }
+
+    @Override
+    public long estimateSize() { return pos; }
+
+    @Override
+    public int characteristics() {
+        return DISTINCT | NONNULL | IMMUTABLE;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/spliterator/SparseArraySubSpliterator.java b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/SparseArraySubSpliterator.java
new file mode 100644
index 0000000000..ac1ffee05b
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/spliterator/SparseArraySubSpliterator.java
@@ -0,0 +1,113 @@
+/*
+ * 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.jena.mem2.spliterator;
+
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+/**
+ * A spliterator for sparse arrays. This spliterator will iterate over the array
+ * skipping null entries.
+ * <p>
+ * This spliterator supports splitting into sub-spliterators.
+ * <p>
+ * The spliterator will check for concurrent modifications by invoking a {@link Runnable}
+ * before each action.
+ *
+ * @param <E>
+ */
+public class SparseArraySubSpliterator<E> implements Spliterator<E> {
+
+    private final E[] entries;
+    private final int fromIndex;
+    private final Runnable checkForConcurrentModification;
+    private int pos;
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param fromIndex                      the index of the first element, inclusive
+     * @param toIndex                        the index of the last element, exclusive
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public SparseArraySubSpliterator(final E[] entries, final int fromIndex, final int toIndex, final Runnable checkForConcurrentModification) {
+        this.entries = entries;
+        this.fromIndex = fromIndex;
+        this.pos = toIndex;
+        this.checkForConcurrentModification = checkForConcurrentModification;
+    }
+
+    /**
+     * Create a spliterator for the given array, with the given size.
+     *
+     * @param entries                        the array
+     * @param checkForConcurrentModification runnable to check for concurrent modifications
+     */
+    public SparseArraySubSpliterator(final E[] entries, final Runnable checkForConcurrentModification) {
+        this(entries, 0, entries.length, checkForConcurrentModification);
+    }
+
+    @Override
+    public boolean tryAdvance(Consumer<? super E> action) {
+        this.checkForConcurrentModification.run();
+        while (fromIndex <= --pos) {
+            if (null != entries[pos]) {
+                action.accept(entries[pos]);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+        pos--;
+        while (fromIndex <= pos) {
+            if (null != entries[pos]) {
+                action.accept(entries[pos]);
+            }
+            pos--;
+        }
+        this.checkForConcurrentModification.run();
+    }
+
+    @Override
+    public Spliterator<E> trySplit() {
+        final int entriesCount = pos - fromIndex;
+        if (entriesCount < 2) {
+            return null;
+        }
+        final int toIndexOfSubIterator = this.pos;
+        this.pos = fromIndex + (entriesCount >>> 1);
+        return new SparseArraySubSpliterator<>(entries, this.pos, toIndexOfSubIterator, checkForConcurrentModification);
+    }
+
+
+    @Override
+    public long estimateSize() {
+        return pos - fromIndex;
+    }
+
+
+    @Override
+    public int characteristics() {
+        return DISTINCT | NONNULL | IMMUTABLE;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/TripleStore.java b/jena-core/src/main/java/org/apache/jena/mem2/store/TripleStore.java
new file mode 100644
index 0000000000..2b67813896
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/TripleStore.java
@@ -0,0 +1,90 @@
+/*
+ * 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.jena.mem2.store;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.stream.Stream;
+
+/**
+ * A triple store is a collection of triples that supports access to
+ * triples matching a triple pattern.
+ */
+public interface TripleStore {
+
+    /**
+     * Add a triple to the map.
+     *
+     * @param triple to add
+     */
+    void add(final Triple triple);
+
+    /**
+     * Remove a triple from the map.
+     *
+     * @param triple to remove
+     */
+    void remove(final Triple triple);
+
+    /**
+     * Remove all triples from the map.
+     */
+    void clear();
+
+    /**
+     * Return the number of triples in the map.
+     */
+    int countTriples();
+
+    /**
+     * Return true if the map is empty.
+     */
+    boolean isEmpty();
+
+
+    /**
+     * Answer true if the graph contains any triple matching <code>t</code>.
+     *
+     * @param tripleMatch triple match pattern, which may be contained
+     */
+    boolean contains(final Triple tripleMatch);
+
+    /**
+     * Returns a {@link Stream} of all triples in the graph.
+     * Note: {@link Stream#parallel()} is supported.
+     *
+     * @return a stream  of triples in this graph.
+     */
+    Stream<Triple> stream();
+
+    /**
+     * Returns a {@link Stream} of Triples matching the given pattern.
+     * Note: {@link Stream#parallel()} is supported.
+     *
+     * @param tripleMatch triple match pattern
+     * @return a stream  of triples in this graph matching the pattern.
+     */
+    Stream<Triple> stream(final Triple tripleMatch);
+
+    /**
+     * Returns an {@link ExtendedIterator} of all triples in the graph matching the given triple match.
+     */
+    ExtendedIterator<Triple> find(final Triple tripleMatch);
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastArrayBunch.java
new file mode 100644
index 0000000000..57035f4223
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastArrayBunch.java
@@ -0,0 +1,192 @@
+/*
+ * 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.jena.mem2.store.fast;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.spliterator.ArraySpliterator;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
+
+import java.util.ConcurrentModificationException;
+import java.util.NoSuchElementException;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * An ArrayBunch implements TripleBunch with a linear search of a short-ish
+ * array of Triples. The array grows by factor 2.
+ */
+public abstract class FastArrayBunch implements FastTripleBunch {
+
+    private static final int INITIAL_SIZE = 4;
+
+    protected int size = 0;
+    protected Triple[] elements;
+
+    protected FastArrayBunch() {
+        elements = new Triple[INITIAL_SIZE];
+    }
+
+    public abstract boolean areEqual(final Triple a, final Triple b);
+
+    @Override
+    public boolean containsKey(Triple t) {
+        int i = size;
+        while (i > 0) if (areEqual(t, elements[--i])) return true;
+        return false;
+    }
+
+    @Override
+    public boolean anyMatch(final Predicate<Triple> predicate) {
+        int i = size;
+        while (i > 0) if (predicate.test(elements[--i])) return true;
+        return false;
+    }
+
+    @Override
+    public boolean anyMatchRandomOrder(Predicate<Triple> predicate) {
+        return anyMatch(predicate);
+    }
+
+    @Override
+    public void clear() {
+        this.elements = new Triple[INITIAL_SIZE];
+        this.size = 0;
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return this.size == 0;
+    }
+
+    @Override
+    public boolean tryAdd(final Triple t) {
+        if (this.containsKey(t)) return false;
+        if (size == elements.length) grow();
+        elements[size++] = t;
+        return true;
+    }
+
+    @Override
+    public void addUnchecked(final Triple t) {
+        if (size == elements.length) grow();
+        elements[size++] = t;
+    }
+
+    /**
+     * Note: linear growth is suboptimal (order n<sup>2</sup>) normally, but
+     * ArrayBunch's are meant for <i>small</i> sets and are replaced by some
+     * sort of hash- or tree- set when they get big; currently "big" means more
+     * than 9 elements, so that's only one growth spurt anyway.
+     */
+    protected void grow() {
+        final var oldElements = elements;
+        elements = new Triple[size << 1];
+        System.arraycopy(oldElements, 0, elements, 0, size);
+    }
+
+    @Override
+    public boolean tryRemove(final Triple t) {
+        for (int i = 0; i < size; i++) {
+            if (areEqual(t, elements[i])) {
+                elements[i] = elements[--size];
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void removeUnchecked(final Triple t) {
+        for (int i = 0; i < size; i++) {
+            if (areEqual(t, elements[i])) {
+                elements[i] = elements[--size];
+                return;
+            }
+        }
+    }
+
+    @Override
+    public ExtendedIterator<Triple> keyIterator() {
+        return new NiceIterator<>() {
+            private final int initialSize = size;
+
+            private int i = size;
+
+            @Override
+            public boolean hasNext() {
+                return 0 < i;
+            }
+
+            @Override
+            public Triple next() {
+                if (size != initialSize) throw new ConcurrentModificationException();
+                if (i == 0) throw new NoSuchElementException();
+                return elements[--i];
+            }
+
+            @Override
+            public void forEachRemaining(Consumer<? super Triple> action) {
+                while (0 < i--) action.accept(elements[i]);
+                if (size != initialSize) throw new ConcurrentModificationException();
+            }
+        };
+
+    }
+
+
+    @Override
+    public Spliterator<Triple> keySpliterator() {
+        final var initialSize = size;
+        final Runnable checkForConcurrentModification = () -> {
+            if (size != initialSize) throw new ConcurrentModificationException();
+        };
+        return new ArraySpliterator<>(elements, size, checkForConcurrentModification);
+    }
+
+    @Override
+    public boolean isArray() {
+        return true;
+    }
+
+    @Override
+    public boolean tryAdd(Triple key, int hashCode) {
+        return tryAdd(key);
+    }
+
+    @Override
+    public void addUnchecked(Triple key, int hashCode) {
+        addUnchecked(key);
+    }
+
+    @Override
+    public boolean tryRemove(Triple key, int hashCode) {
+        return tryRemove(key);
+    }
+
+    @Override
+    public void removeUnchecked(Triple key, int hashCode) {
+        removeUnchecked(key);
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastHashedBunchMap.java
similarity index 61%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastHashedBunchMap.java
index f601955053..a9e598dce6 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastHashedBunchMap.java
@@ -15,18 +15,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.store.fast;
 
-import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.FastHashMap;
 
 /**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
+ * Map from nodes to triple bunches.
  */
-public class Releases {
+public class FastHashedBunchMap extends FastHashMap<Node, FastTripleBunch> {
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    public FastHashedBunchMap() {
+        super();
+    }
 
+    @Override
+    protected Node[] newKeysArray(int size) {
+        return new Node[size];
+    }
+
+    @Override
+    protected FastTripleBunch[] newValuesArray(int size) {
+        return new FastTripleBunch[size];
+    }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastHashedTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastHashedTripleBunch.java
new file mode 100644
index 0000000000..752a57971c
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastHashedTripleBunch.java
@@ -0,0 +1,51 @@
+/*
+ * 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.jena.mem2.store.fast;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.FastHashSet;
+import org.apache.jena.mem2.collection.JenaSet;
+
+/**
+ * A set of triples - backed by {@link FastHashSet}.
+ */
+public class FastHashedTripleBunch extends FastHashSet<Triple> implements FastTripleBunch {
+    /**
+     * Create a new triple bunch from the given set of triples.
+     *
+     * @param set the set of triples
+     */
+    public FastHashedTripleBunch(final JenaSet<Triple> set) {
+        super((set.size() >> 1) + set.size()); //it should not only fit but also have some space for growth
+        set.keyIterator().forEachRemaining(this::addUnchecked);
+    }
+
+    public FastHashedTripleBunch() {
+        super();
+    }
+
+    @Override
+    protected Triple[] newKeysArray(int size) {
+        return new Triple[size];
+    }
+
+    @Override
+    public boolean isArray() {
+        return false;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastTripleBunch.java b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastTripleBunch.java
new file mode 100644
index 0000000000..116873f1db
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastTripleBunch.java
@@ -0,0 +1,51 @@
+/*
+ * 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.jena.mem2.store.fast;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.JenaMapSetCommon;
+import org.apache.jena.mem2.collection.JenaSetHashOptimized;
+
+import java.util.function.Predicate;
+
+/**
+ * A bunch of triples - a stripped-down set with specialized methods. A
+ * bunch is expected to store triples that share some useful property
+ * (such as having the same subject or predicate).
+ */
+public interface FastTripleBunch extends JenaSetHashOptimized<Triple> {
+    /**
+     * Answer true iff this bunch is implemented as an array.
+     * This field is used to optimize some operations by avoiding the need for instanceOf tests.
+     *
+     * @return true iff this bunch is implemented as an arrays
+     */
+    boolean isArray();
+
+    /**
+     * This method is used to optimize _PO match operations.
+     * The {@link JenaMapSetCommon#anyMatch(Predicate)} method is faster if there are only a few matches.
+     * This method is faster if there are many matches and the set is ordered in an unfavorable way.
+     * _PO matches usually fall into this category.
+     *
+     * @param predicate the predicate to match
+     * @return true if any triple in the bunch matches the predicate
+     */
+    boolean anyMatchRandomOrder(Predicate<Triple> predicate);
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastTripleStore.java
new file mode 100644
index 0000000000..00e1c48818
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/fast/FastTripleStore.java
@@ -0,0 +1,386 @@
+/*
+ * 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.jena.mem2.store.fast;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.FastHashMap;
+import org.apache.jena.mem2.iterator.IteratorOfJenaSets;
+import org.apache.jena.mem2.pattern.PatternClassifier;
+import org.apache.jena.mem2.store.TripleStore;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
+import org.apache.jena.util.iterator.SingletonIterator;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * A triple store that uses hash tables to map from nodes to triple bunches.
+ * <p>
+ * Inner structure:
+ * - three {@link FastHashMap}, one for each node (subject, predicate, object) in the triple
+ * - each map maps from a node to a {@link FastTripleBunch}
+ * - for up to 16 triples with the same subject, the bunch is an {@link FastArrayBunch}, otherwise it is
+ * a {@link FastHashedTripleBunch}
+ * - for up to 32 triples with the same predicate and object, the bunch is an {@link FastArrayBunch}, otherwise it is
+ * a {@link FastHashedTripleBunch}
+ * <p>
+ * Other optimizations:
+ * - each triple is added to three {@link FastTripleBunch}es. To avoid the overhead of calculating the hash code three
+ * times, the hash code is calculated once and passed to the {@link FastTripleBunch}es.
+ * - the different sizes for the {@link FastArrayBunch}es are chosen:
+ * - to avoid the memory-overhead of {@link FastHashedTripleBunch} for a small number of triples
+ * - the subject bunch is smaller than the predicate and object bunches, because the subject is typically used for
+ * #contains operations, which are faster for {@link FastHashedTripleBunch}es.
+ * - the predicate and object bunches are the same size, because they are typically used for #find operations, which
+ * typically do not answer #contains operations. Making them much larger might slow down #remove operations on the
+ * graph.
+ * - "ANY_PRE_OBJ" matches primarily use the object bunch unless the size of the bunch is larger than 400 triples. In
+ * that case, there is a secondary lookup in the predicate bunch. Then the smaller bunch is used for the lookup.
+ * Especially for RDF graphs there are some very common object nodes like "true"/"false" or "0"/"1". In that case,
+ * a secondary lookup in the predicate bunch might be faster than a primary lookup in the object bunch.
+ * - FastTripleStore#contains uses {@link org.apache.jena.mem2.store.fast.FastTripleBunch#anyMatchRandomOrder} for
+ * "ANY_PRE_OBJ" lookups. This is only faster than {@link org.apache.jena.mem2.collection.JenaMapSetCommon#anyMatch}
+ * if there are many matches and the set is ordered in an unfavorable way. "ANY_PRE_OBJ" matches usually fall into
+ * this category. This optimization was only needed because the {@link FastTripleBunch}es does not use the random
+ * order of the triples in #anyMatch but the ordered dense array of triples, which is faster if there are only a few
+ * matches.
+ * - for the FastArrayBunches, the equals method of the triple is not called. Instead, only the two nodes that are
+ *   not part of the key of the containing map are compared.
+ */
+public class FastTripleStore implements TripleStore {
+
+    protected static final int THRESHOLD_FOR_SECONDARY_LOOKUP = 400;
+    protected static final int MAX_ARRAY_BUNCH_SIZE_SUBJECT = 16;
+    protected static final int MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT = 32;
+    final FastHashedBunchMap subjects = new FastHashedBunchMap();
+    final FastHashedBunchMap predicates = new FastHashedBunchMap();
+    final FastHashedBunchMap objects = new FastHashedBunchMap();
+    private int size = 0;
+
+    @Override
+    public void add(Triple triple) {
+        final int hashCodeOfTriple = triple.hashCode();
+        final boolean added;
+        var sBunch = subjects.get(triple.getSubject());
+        if (sBunch == null) {
+            sBunch = new ArrayBunchWithSameSubject();
+            sBunch.addUnchecked(triple, hashCodeOfTriple);
+            subjects.put(triple.getSubject(), sBunch);
+            added = true;
+        } else {
+            if (sBunch.isArray() && sBunch.size() == MAX_ARRAY_BUNCH_SIZE_SUBJECT) {
+                sBunch = new FastHashedTripleBunch(sBunch);
+                subjects.put(triple.getSubject(), sBunch);
+            }
+            added = sBunch.tryAdd(triple, hashCodeOfTriple);
+        }
+        if (added) {
+            size++;
+            var pBunch = predicates.computeIfAbsent(triple.getPredicate(), ArrayBunchWithSamePredicate::new);
+            if (pBunch.isArray() && pBunch.size() == MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT) {
+                pBunch = new FastHashedTripleBunch(pBunch);
+                predicates.put(triple.getPredicate(), pBunch);
+            }
+            pBunch.addUnchecked(triple, hashCodeOfTriple);
+            var oBunch = objects.computeIfAbsent(triple.getObject(), ArrayBunchWithSameObject::new);
+            if (oBunch.isArray() && oBunch.size() == MAX_ARRAY_BUNCH_SIZE_PREDICATE_OBJECT) {
+                oBunch = new FastHashedTripleBunch(oBunch);
+                objects.put(triple.getObject(), oBunch);
+            }
+            oBunch.addUnchecked(triple, hashCodeOfTriple);
+        }
+    }
+
+    @Override
+    public void remove(Triple triple) {
+        final int hashCodeOfTriple = triple.hashCode();
+        final var sBunch = subjects.get(triple.getSubject());
+        if (sBunch == null)
+            return;
+
+        if (sBunch.tryRemove(triple, hashCodeOfTriple)) {
+            if (sBunch.isEmpty()) {
+                subjects.removeUnchecked(triple.getSubject());
+            }
+            final var pBunch = predicates.get(triple.getPredicate());
+            pBunch.removeUnchecked(triple, hashCodeOfTriple);
+            if (pBunch.isEmpty()) {
+                predicates.removeUnchecked(triple.getPredicate());
+            }
+            final var oBunch = objects.get(triple.getObject());
+            oBunch.removeUnchecked(triple, hashCodeOfTriple);
+            if (oBunch.isEmpty()) {
+                objects.removeUnchecked(triple.getObject());
+            }
+            size--;
+        }
+    }
+
+    @Override
+    public void clear() {
+        subjects.clear();
+        predicates.clear();
+        objects.clear();
+        size = 0;
+    }
+
+    @Override
+    public int countTriples() {
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size == 0;
+    }
+
+    @Override
+    public boolean contains(Triple tripleMatch) {
+        switch (PatternClassifier.classify(tripleMatch)) {
+
+            case SUB_PRE_OBJ: {
+                final var triples = subjects.get(tripleMatch.getSubject());
+                if (triples == null) {
+                    return false;
+                }
+                return triples.containsKey(tripleMatch);
+            }
+
+            case SUB_PRE_ANY: {
+                final var triplesBySubject = subjects.get(tripleMatch.getSubject());
+                if (triplesBySubject == null) {
+                    return false;
+                }
+                return triplesBySubject.anyMatch(t -> tripleMatch.getPredicate().equals(t.getPredicate()));
+            }
+
+            case SUB_ANY_OBJ: {
+                final var triplesBySubject = subjects.get(tripleMatch.getSubject());
+                if (triplesBySubject == null) {
+                    return false;
+                }
+                return triplesBySubject.anyMatch(t -> tripleMatch.getObject().equals(t.getObject()));
+            }
+
+            case SUB_ANY_ANY:
+                return subjects.containsKey(tripleMatch.getSubject());
+
+            case ANY_PRE_OBJ: {
+                final var triplesByObject = objects.get(tripleMatch.getObject());
+                if (triplesByObject == null) {
+                    return false;
+                }
+                // Optimization for typical RDF data, where there may be common values like "0" or "false"/"true".
+                // In this case, there may be many matches but due to the ordered nature of FastHashBase,
+                // the same predicates are often grouped together. If they are at the beginning of the bunch,
+                // we can avoid the linear scan of the bunch. This is a common case for RDF data.
+                // #anyMatchRandomOrder is a bit slower if the predicate is not found than #anyMatch, but not by much.
+                if (triplesByObject.size() > THRESHOLD_FOR_SECONDARY_LOOKUP) {
+                    final var triplesByPredicate = predicates.get(tripleMatch.getPredicate());
+                    if (triplesByPredicate == null) {
+                        return false;
+                    }
+                    if (triplesByPredicate.size() < triplesByObject.size()) {
+                        return triplesByPredicate.anyMatchRandomOrder(t -> tripleMatch.getObject().equals(t.getObject()));
+                    }
+                }
+                return triplesByObject.anyMatchRandomOrder(t -> tripleMatch.getPredicate().equals(t.getPredicate()));
+            }
+
+            case ANY_PRE_ANY:
+                return predicates.containsKey(tripleMatch.getPredicate());
+
+            case ANY_ANY_OBJ:
+                return objects.containsKey(tripleMatch.getObject());
+
+            case ANY_ANY_ANY:
+                return !isEmpty();
+
+            default:
+                throw new IllegalStateException(String.format("Unexpected value: %s", PatternClassifier.classify(tripleMatch)));
+        }
+    }
+
+    @Override
+    public Stream<Triple> stream() {
+        return StreamSupport.stream(subjects.valueSpliterator(), false)
+                .flatMap(bunch -> StreamSupport.stream(bunch.keySpliterator(), false));
+    }
+
+    @Override
+    public Stream<Triple> stream(Triple tripleMatch) {
+        switch (PatternClassifier.classify(tripleMatch)) {
+
+            case SUB_PRE_OBJ: {
+                final var triples = subjects.get(tripleMatch.getSubject());
+                if (triples == null) {
+                    return Stream.empty();
+                }
+                return triples.containsKey(tripleMatch) ? Stream.of(tripleMatch) : Stream.empty();
+            }
+
+            case SUB_PRE_ANY: {
+                final var triplesBySubject = subjects.get(tripleMatch.getSubject());
+                if (triplesBySubject == null) {
+                    return Stream.empty();
+                }
+                return triplesBySubject.keyStream().filter(t -> tripleMatch.getPredicate().equals(t.getPredicate()));
+            }
+
+            case SUB_ANY_OBJ: {
+                final var triplesBySubject = subjects.get(tripleMatch.getSubject());
+                if (triplesBySubject == null) {
+                    return Stream.empty();
+                }
+                return triplesBySubject.keyStream().filter(t -> tripleMatch.getObject().equals(t.getObject()));
+            }
+
+            case SUB_ANY_ANY: {
+                final var triples = subjects.get(tripleMatch.getSubject());
+                return triples == null ? Stream.empty() : triples.keyStream();
+            }
+
+            case ANY_PRE_OBJ: {
+                final var triplesByObject = objects.get(tripleMatch.getObject());
+                if (triplesByObject == null) {
+                    return Stream.empty();
+                }
+                if (triplesByObject.size() > THRESHOLD_FOR_SECONDARY_LOOKUP) {
+                    final var triplesByPredicate = predicates.get(tripleMatch.getPredicate());
+                    if (triplesByPredicate == null) {
+                        return Stream.empty();
+                    }
+                    if (triplesByPredicate.size() < triplesByObject.size()) {
+                        return triplesByPredicate.keyStream().filter(t -> tripleMatch.getObject().equals(t.getObject()));
+                    }
+                }
+                return triplesByObject.keyStream().filter(t -> tripleMatch.getPredicate().equals(t.getPredicate()));
+            }
+
+            case ANY_PRE_ANY: {
+                final var triples = predicates.get(tripleMatch.getPredicate());
+                return triples == null ? Stream.empty() : triples.keyStream();
+            }
+
+            case ANY_ANY_OBJ: {
+                final var triples = objects.get(tripleMatch.getObject());
+                return triples == null ? Stream.empty() : triples.keyStream();
+            }
+
+            case ANY_ANY_ANY:
+                return stream();
+
+            default:
+                throw new IllegalStateException("Unexpected value: " + PatternClassifier.classify(tripleMatch));
+        }
+    }
+
+    @Override
+    public ExtendedIterator<Triple> find(Triple tripleMatch) {
+        switch (PatternClassifier.classify(tripleMatch)) {
+
+            case SUB_PRE_OBJ: {
+                final var triples = subjects.get(tripleMatch.getSubject());
+                if (triples == null) {
+                    return NiceIterator.emptyIterator();
+                }
+                return triples.containsKey(tripleMatch) ? new SingletonIterator<>(tripleMatch) : NiceIterator.emptyIterator();
+            }
+
+            case SUB_PRE_ANY: {
+                final var triplesBySubject = subjects.get(tripleMatch.getSubject());
+                if (triplesBySubject == null) {
+                    return NiceIterator.emptyIterator();
+                }
+                return triplesBySubject.keyIterator().filterKeep(t -> tripleMatch.getPredicate().equals(t.getPredicate()));
+            }
+
+            case SUB_ANY_OBJ: {
+                final var triplesBySubject = subjects.get(tripleMatch.getSubject());
+                if (triplesBySubject == null) {
+                    return NiceIterator.emptyIterator();
+                }
+                return triplesBySubject.keyIterator().filterKeep(t -> tripleMatch.getObject().equals(t.getObject()));
+            }
+
+            case SUB_ANY_ANY: {
+                final var triples = subjects.get(tripleMatch.getSubject());
+                return triples == null ? NiceIterator.emptyIterator() : triples.keyIterator();
+            }
+
+            case ANY_PRE_OBJ: {
+                final var triplesByObject = objects.get(tripleMatch.getObject());
+                if (triplesByObject == null) {
+                    return NiceIterator.emptyIterator();
+                }
+                if (triplesByObject.size() > THRESHOLD_FOR_SECONDARY_LOOKUP) {
+                    final var triplesByPredicate = predicates.get(tripleMatch.getPredicate());
+                    if (triplesByPredicate == null) {
+                        return NiceIterator.emptyIterator();
+                    }
+                    if (triplesByPredicate.size() < triplesByObject.size()) {
+                        return triplesByPredicate.keyIterator().filterKeep(t -> tripleMatch.getObject().equals(t.getObject()));
+                    }
+                }
+                return triplesByObject.keyIterator().filterKeep(t -> tripleMatch.getPredicate().equals(t.getPredicate()));
+            }
+
+            case ANY_PRE_ANY: {
+                final var triples = predicates.get(tripleMatch.getPredicate());
+                return triples == null ? NiceIterator.emptyIterator() : triples.keyIterator();
+            }
+
+            case ANY_ANY_OBJ: {
+                final var triples = objects.get(tripleMatch.getObject());
+                return triples == null ? NiceIterator.emptyIterator() : triples.keyIterator();
+            }
+
+            case ANY_ANY_ANY:
+                return new IteratorOfJenaSets<>(subjects.valueIterator());
+
+            default:
+                throw new IllegalStateException("Unexpected value: " + PatternClassifier.classify(tripleMatch));
+        }
+    }
+
+    protected static class ArrayBunchWithSameSubject extends FastArrayBunch {
+
+        @Override
+        public boolean areEqual(final Triple a, final Triple b) {
+            return a.getPredicate().equals(b.getPredicate())
+                    && a.getObject().equals(b.getObject());
+        }
+    }
+
+    protected static class ArrayBunchWithSamePredicate extends FastArrayBunch {
+        @Override
+        public boolean areEqual(final Triple a, final Triple b) {
+            return a.getSubject().equals(b.getSubject())
+                    && a.getObject().equals(b.getObject());
+        }
+    }
+
+    protected static class ArrayBunchWithSameObject extends FastArrayBunch {
+        @Override
+        public boolean areEqual(final Triple a, final Triple b) {
+            return a.getSubject().equals(b.getSubject())
+                    && a.getPredicate().equals(b.getPredicate());
+        }
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/ArrayBunch.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/ArrayBunch.java
new file mode 100644
index 0000000000..83ca78aa3f
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/ArrayBunch.java
@@ -0,0 +1,166 @@
+/*
+ * 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.jena.mem2.store.legacy;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.spliterator.ArraySpliterator;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
+
+import java.util.ConcurrentModificationException;
+import java.util.NoSuchElementException;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * An ArrayBunch implements TripleBunch with a linear search of a short-ish
+ * array of Triples. The array can grow, but it only grows by 4 elements each time
+ * (because, if it gets big enough for this linear growth to be bad, it should always
+ * have been replaced by a more efficient set-of-triples implementation).
+ */
+public class ArrayBunch implements TripleBunch {
+
+    private static final int INITIAL_SIZE = 5;
+
+    protected int size = 0;
+    protected Triple[] elements;
+
+    public ArrayBunch() {
+        elements = new Triple[INITIAL_SIZE];
+    }
+
+    @Override
+    public boolean containsKey(Triple t) {
+        int i = size;
+        while (i > 0) if (t.equals(elements[--i])) return true;
+        return false;
+    }
+
+    @Override
+    public boolean anyMatch(Predicate<Triple> predicate) {
+        int i = size;
+        while (i > 0) if (predicate.test(elements[--i])) return true;
+        return false;
+    }
+
+    @Override
+    public void clear() {
+        this.elements = new Triple[INITIAL_SIZE];
+        this.size = 0;
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return this.size == 0;
+    }
+
+    @Override
+    public boolean tryAdd(Triple t) {
+        if (this.containsKey(t)) return false;
+        if (size == elements.length) grow();
+        elements[size++] = t;
+        return true;
+    }
+
+    @Override
+    public void addUnchecked(Triple t) {
+        if (size == elements.length) grow();
+        elements[size++] = t;
+    }
+
+    /**
+     * Note: linear growth is suboptimal (order n<sup>2</sup>) normally, but
+     * ArrayBunch's are meant for <i>small</i> sets and are replaced by some
+     * sort of hash- or tree- set when they get big; currently "big" means more
+     * than 9 elements, so that's only one growth spurt anyway.
+     */
+    protected void grow() {
+        final var oldElements = elements;
+        elements = new Triple[size + 4];
+        System.arraycopy(oldElements, 0, elements, 0, size);
+    }
+
+    @Override
+    public boolean tryRemove(Triple t) {
+        for (int i = 0; i < size; i += 1) {
+            if (t.equals(elements[i])) {
+                elements[i] = elements[--size];
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void removeUnchecked(Triple t) {
+        for (int i = 0; i < size; i += 1) {
+            if (t.equals(elements[i])) {
+                elements[i] = elements[--size];
+                return;
+            }
+        }
+    }
+
+    @Override
+    public ExtendedIterator<Triple> keyIterator() {
+        return new NiceIterator<>() {
+            private final int initialSize = size;
+
+            private int i = size;
+
+            @Override
+            public boolean hasNext() {
+                return 0 < i;
+            }
+
+            @Override
+            public Triple next() {
+                if (size != initialSize) throw new ConcurrentModificationException();
+                if (i == 0) throw new NoSuchElementException();
+                return elements[--i];
+            }
+
+            @Override
+            public void forEachRemaining(Consumer<? super Triple> action) {
+                while (0 < i--) action.accept(elements[i]);
+                if (size != initialSize) throw new ConcurrentModificationException();
+            }
+        };
+    }
+
+    @Override
+    public Spliterator<Triple> keySpliterator() {
+        final var initialSize = size;
+        final Runnable checkForConcurrentModification = () -> {
+            if (size != initialSize) throw new ConcurrentModificationException();
+        };
+        return new ArraySpliterator<>(elements, size, checkForConcurrentModification);
+    }
+
+    @Override
+    public boolean isArray() {
+        return true;
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/FieldFilter.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/FieldFilter.java
new file mode 100644
index 0000000000..3111803c03
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/FieldFilter.java
@@ -0,0 +1,69 @@
+/*
+ * 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.jena.mem2.store.legacy;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+import java.util.function.Predicate;
+
+/**
+ * A class that encapsulates a filter on fields on a triple.
+ * <p>
+ * The filter is a predicate that takes a triple and returns true if it passes
+ * the filter and false otherwise.
+ * </p>
+ */
+public class FieldFilter {
+
+    public static final FieldFilter EMPTY = new FieldFilter();
+
+    private final Predicate<Triple> filter;
+
+    private final boolean hasFilter;
+
+    private FieldFilter(Predicate<Triple> filter) {
+        this.filter = filter;
+        this.hasFilter = true;
+    }
+
+    private FieldFilter() {
+        this.filter = null;
+        this.hasFilter = false;
+    }
+
+    public static FieldFilter filterOn(Triple.Field f1, Node n1, Triple.Field f2, Node n2) {
+        if (n1.isConcrete()) {
+            if (n2.isConcrete()) {
+                return new FieldFilter(t -> n1.equals(f1.getField(t)) && n2.equals(f2.getField(t)));
+            }
+            return new FieldFilter(t -> n1.equals(f1.getField(t)));
+        } else if (n2.isConcrete()) {
+            return new FieldFilter(t -> n2.equals(f2.getField(t)));
+        }
+        return FieldFilter.EMPTY;
+    }
+
+    public boolean hasFilter() {
+        return hasFilter;
+    }
+
+    public Predicate<Triple> getFilter() {
+        return filter;
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/HashedBunchMap.java
similarity index 60%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/main/java/org/apache/jena/mem2/store/legacy/HashedBunchMap.java
index f601955053..d719d74772 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/HashedBunchMap.java
@@ -15,18 +15,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.store.legacy;
 
-import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.HashCommonMap;
 
 /**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
+ * A map from nodes to bunches of triples.
  */
-public class Releases {
+public class HashedBunchMap extends HashCommonMap<Node, TripleBunch> {
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    public HashedBunchMap() {
+        super(10);
+    }
 
+    @Override
+    protected Node[] newKeysArray(int size) {
+        return new Node[size];
+    }
+
+    @Override
+    protected TripleBunch[] newValuesArray(int size) {
+        return new TripleBunch[size];
+    }
+
+    @Override
+    public void clear() {
+        super.clear(10);
+    }
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/HashedTripleBunch.java
similarity index 52%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/main/java/org/apache/jena/mem2/store/legacy/HashedTripleBunch.java
index f601955053..9bf8eb3e6a 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/HashedTripleBunch.java
@@ -15,18 +15,37 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2.store.legacy;
 
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.HashCommonSet;
+import org.apache.jena.mem2.collection.JenaSet;
 
 /**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
+ * A bunch of triples, implemented as a set of triples.
  */
-public class Releases {
+public class HashedTripleBunch extends HashCommonSet<Triple> implements TripleBunch {
+    protected HashedTripleBunch(final JenaSet<Triple> b) {
+        super(nextSize((int) (b.size() / LOAD_FACTOR)));
+        b.keyIterator().forEachRemaining(this::addUnchecked);
+    }
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+    public HashedTripleBunch() {
+        super(8);
+    }
 
+    @Override
+    protected Triple[] newKeysArray(int size) {
+        return new Triple[size];
+    }
+
+    @Override
+    public void clear() {
+        super.clear(8);
+    }
+
+    @Override
+    public boolean isArray() {
+        return false;
+    }
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/LegacyTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/LegacyTripleStore.java
new file mode 100644
index 0000000000..a8a9e91980
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/LegacyTripleStore.java
@@ -0,0 +1,152 @@
+/*
+ * 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.jena.mem2.store.legacy;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.HashCommonMap;
+import org.apache.jena.mem2.collection.JenaSet;
+import org.apache.jena.mem2.store.TripleStore;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
+import org.apache.jena.util.iterator.SingletonIterator;
+
+import java.util.stream.Stream;
+
+/**
+ * Successor of {@link org.apache.jena.mem.GraphTripleStoreMem} that uses term-equality
+ * instead of literal value equality.
+ * This implementation also does not support {@link java.util.Iterator#remove()}.
+ * <p>
+ * Inner structure:
+ * - three {@link NodeToTriplesMapMem} instances for each of the three triple fields (subject, predicate, object)
+ * - each of these maps is a {@link HashCommonMap} with {@link Node} keys and {@link JenaSet} values.
+ * - for up to 9 triples with the same subject, predicate or object, the {@link JenaSet} is
+ * a {@link ArrayBunch}, otherwise it is a {@link HashedTripleBunch}.
+ * <p>
+ * Additional optimizations:
+ * - because we know that if a triple exists in one of the maps, it also exists in the other two, we can use the
+ * {@link org.apache.jena.mem2.collection.JenaSet#addUnchecked(java.lang.Object)} and
+ * {@link org.apache.jena.mem2.collection.JenaMapSetCommon#removeUnchecked(java.lang.Object)} methods to avoid
+ * unnecessary checks.
+ */
+public class LegacyTripleStore implements TripleStore {
+
+    private final NodeToTriplesMap subjects
+            = new NodeToTriplesMapMem(Triple.Field.fieldSubject, Triple.Field.fieldPredicate, Triple.Field.fieldObject);
+    private final NodeToTriplesMap predicates
+            = new NodeToTriplesMapMem(Triple.Field.fieldPredicate, Triple.Field.fieldObject, Triple.Field.fieldSubject);
+    private final NodeToTriplesMap objects
+            = new NodeToTriplesMapMem(Triple.Field.fieldObject, Triple.Field.fieldSubject, Triple.Field.fieldPredicate);
+
+    @Override
+    public void add(Triple triple) {
+        if (subjects.tryAdd(triple)) {
+            predicates.addUnchecked(triple);
+            objects.addUnchecked(triple);
+        }
+    }
+
+    @Override
+    public void remove(Triple triple) {
+        if (subjects.tryRemove(triple)) {
+            predicates.removeUnchecked(triple);
+            objects.removeUnchecked(triple);
+        }
+    }
+
+    @Override
+    public void clear() {
+        subjects.clear();
+        predicates.clear();
+        objects.clear();
+    }
+
+    @Override
+    public int countTriples() {
+        return subjects.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return subjects.isEmpty();
+    }
+
+    @Override
+    public boolean contains(Triple tripleMatch) {
+        if (tripleMatch.isConcrete()) {
+            return subjects.containsKey(tripleMatch);
+        }
+
+        final Node pm = tripleMatch.getPredicate();
+        final Node om = tripleMatch.getObject();
+        final Node sm = tripleMatch.getSubject();
+        if (sm.isConcrete())
+            return subjects.containsMatch(sm, pm, om);
+        else if (om.isConcrete())
+            return objects.containsMatch(om, sm, pm);
+        else if (pm.isConcrete())
+            return predicates.containsMatch(pm, om, sm);
+        else
+            return !this.isEmpty();
+    }
+
+    @Override
+    public Stream<Triple> stream() {
+        return subjects.keyStream();
+    }
+
+    @Override
+    public Stream<Triple> stream(Triple tripleMatch) {
+        if (tripleMatch.isConcrete()) {
+            return subjects.containsKey(tripleMatch) ? Stream.of(tripleMatch) : Stream.empty();
+        }
+
+        final Node pm = tripleMatch.getPredicate();
+        final Node om = tripleMatch.getObject();
+        final Node sm = tripleMatch.getSubject();
+
+        if (sm.isConcrete())
+            return subjects.streamForMatches(sm, pm, om);
+        else if (om.isConcrete())
+            return objects.streamForMatches(om, sm, pm);
+        else if (pm.isConcrete())
+            return predicates.streamForMatches(pm, om, sm);
+        else
+            return subjects.keyStream();
+    }
+
+    @Override
+    public ExtendedIterator<Triple> find(Triple tripleMatch) {
+        if (tripleMatch.isConcrete()) {
+            return subjects.containsKey(tripleMatch) ? new SingletonIterator<>(tripleMatch) : NiceIterator.emptyIterator();
+        }
+        final Node pm = tripleMatch.getPredicate();
+        final Node om = tripleMatch.getObject();
+        final Node sm = tripleMatch.getSubject();
+
+        if (sm.isConcrete())
+            return subjects.iteratorForMatches(sm, pm, om);
+        else if (om.isConcrete())
+            return objects.iteratorForMatches(om, sm, pm);
+        else if (pm.isConcrete())
+            return predicates.iteratorForMatches(pm, om, sm);
+        else
+            return subjects.keyIterator();
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/NodeToTriplesMap.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/NodeToTriplesMap.java
new file mode 100644
index 0000000000..d2bd89b966
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/NodeToTriplesMap.java
@@ -0,0 +1,61 @@
+/*
+ * 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.jena.mem2.store.legacy;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.JenaSet;
+import org.apache.jena.util.iterator.ExtendedIterator;
+
+import java.util.stream.Stream;
+
+/**
+ * A map from a node to the triples that have that node as subject, predicate or object.
+ */
+public interface NodeToTriplesMap extends JenaSet<Triple> {
+
+    /**
+     * Answer an iterator over all the triples in this map that match the pattern.
+     *
+     * @param index The node to match as key.
+     * @param n2    A node to match, or Node.ANY.
+     * @param n3    A node to match, or Node.ANY.
+     * @return An iterator over all the triples in this map that match the pattern.
+     */
+    ExtendedIterator<Triple> iteratorForMatches(Node index, Node n2, Node n3);
+
+    /**
+     * Answer a stream over all the triples in this map that match the pattern.
+     *
+     * @param index The node to match as key.
+     * @param n2    A node to match, or Node.ANY.
+     * @param n3    A node to match, or Node.ANY.
+     * @return A stream over all the triples in this map that match the pattern.
+     */
+    Stream<Triple> streamForMatches(Node index, Node n2, Node n3);
+
+    /**
+     * Answer true iff this map contains a triple that matches the pattern.
+     *
+     * @param index The node to match as key.
+     * @param n2    A node to match, or Node.ANY.
+     * @param n3    A node to match, or Node.ANY.
+     * @return True iff this map contains a triple that matches the pattern.
+     */
+    boolean containsMatch(Node index, Node n2, Node n3);
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/NodeToTriplesMapMem.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/NodeToTriplesMapMem.java
new file mode 100644
index 0000000000..bb66bc1555
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/NodeToTriplesMapMem.java
@@ -0,0 +1,201 @@
+/*
+ * 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.jena.mem2.store.legacy;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.JenaMap;
+import org.apache.jena.mem2.iterator.IteratorOfJenaSets;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NullIterator;
+
+import java.util.Spliterator;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+public class NodeToTriplesMapMem implements NodeToTriplesMap {
+
+    private final JenaMap<Node, TripleBunch> bunchMap = new HashedBunchMap();
+    private final Triple.Field indexField;
+    private final Triple.Field f2;
+    private final Triple.Field f3;
+
+    /**
+     * The number of triples held in this NTM, maintained incrementally
+     * (because it's a pain to compute from scratch).
+     */
+    private int size = 0;
+
+    public NodeToTriplesMapMem(Triple.Field indexField, Triple.Field f2, Triple.Field f3) {
+        this.indexField = indexField;
+        this.f2 = f2;
+        this.f3 = f3;
+    }
+
+    private Node getIndexNode(Triple t) {
+        return indexField.getField(t);
+    }
+
+    @Override
+    public void clear() {
+        this.bunchMap.clear();
+        this.size = 0;
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return this.size == 0;
+    }
+
+    @Override
+    @SuppressWarnings("squid:S1121")
+    public boolean tryAdd(Triple t) {
+        final Node node = getIndexNode(t);
+
+        TripleBunch s = bunchMap.get(node);
+        if (s == null) {
+            bunchMap.put(node, s = new ArrayBunch());
+            s.addUnchecked(t);
+            size++;
+            return true;
+        }
+
+        if ((s.isArray()) && s.size() == 9) {
+            bunchMap.put(node, s = new HashedTripleBunch(s));
+        }
+        if (s.tryAdd(t)) {
+            size++;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void addUnchecked(Triple t) {
+        final Node node = getIndexNode(t);
+        TripleBunch s = bunchMap.get(node);
+        if (s == null) {
+            s = new ArrayBunch();
+            bunchMap.put(node, s);
+        } else if ((s.isArray()) && s.size() == 9) {
+            s = new HashedTripleBunch(s);
+            bunchMap.put(node, s);
+        }
+        s.addUnchecked(t);
+        size++;
+    }
+
+    @Override
+    public boolean tryRemove(Triple t) {
+        final Node node = getIndexNode(t);
+        final TripleBunch s = bunchMap.get(node);
+
+        if (s == null)
+            return false;
+
+        if (s.tryRemove(t)) {
+            size--;
+            if (s.isEmpty()) bunchMap.removeUnchecked(node);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void removeUnchecked(Triple t) {
+        final Node node = getIndexNode(t);
+        final TripleBunch s = bunchMap.get(node);
+
+        if (s == null)
+            return;
+
+        s.removeUnchecked(t);
+        size--;
+        if (s.isEmpty()) bunchMap.removeUnchecked(node);
+    }
+
+    @Override
+    public ExtendedIterator<Triple> keyIterator() {
+        return new IteratorOfJenaSets<>(bunchMap.valueIterator());
+    }
+
+    @Override
+    public Spliterator<Triple> keySpliterator() {
+        return keyStream().spliterator();
+    }
+
+    @Override
+    public Stream<Triple> keyStream() {
+        return StreamSupport.stream(bunchMap.valueSpliterator(), false)
+                .flatMap(bunch -> StreamSupport.stream(bunch.keySpliterator(), false));
+    }
+
+    @Override
+    public ExtendedIterator<Triple> iteratorForMatches(Node index, Node n2, Node n3) {
+        final TripleBunch s = bunchMap.get(index);
+
+        if (s == null) return NullIterator.instance();
+
+        final var filter = FieldFilter.filterOn(f2, n2, f3, n3);
+        return filter.hasFilter()
+                ? s.keyIterator().filterKeep(filter.getFilter())
+                : s.keyIterator();
+    }
+
+    @Override
+    public Stream<Triple> streamForMatches(Node index, Node n2, Node n3) {
+        final TripleBunch s = bunchMap.get(index);
+        if (s == null)
+            return Stream.empty();
+        final var filter = FieldFilter.filterOn(f2, n2, f3, n3);
+        return filter.hasFilter()
+                ? StreamSupport.stream(s.keySpliterator(), false).filter(filter.getFilter())
+                : StreamSupport.stream(s.keySpliterator(), false);
+    }
+
+    @Override
+    public boolean containsMatch(Node index, Node n2, Node n3) {
+        final TripleBunch s = bunchMap.get(index);
+        if (s == null)
+            return false;
+        var filter = FieldFilter.filterOn(f2, n2, f3, n3);
+        if (!filter.hasFilter())
+            return true;
+        return s.anyMatch(filter.getFilter());
+    }
+
+    @Override
+    public boolean containsKey(Triple triple) {
+        final TripleBunch s = bunchMap.get(getIndexNode(triple));
+        if (s == null)
+            return false;
+
+        return s.containsKey(triple);
+    }
+
+    @Override
+    public boolean anyMatch(Predicate<Triple> predicate) {
+        return bunchMap.valueStream().anyMatch(bunch -> bunch.anyMatch(predicate));
+    }
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/TripleBunch.java
similarity index 58%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/main/java/org/apache/jena/mem2/store/legacy/TripleBunch.java
index f601955053..0c595cf3ac 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/legacy/TripleBunch.java
@@ -15,18 +15,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
 
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
+package org.apache.jena.mem2.store.legacy;
+
 import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.JenaSet;
 
 /**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
+ * A bunch of triples - a stripped-down set with specialized methods. A
+ * bunch is expected to store triples that share some useful property
+ * (such as having the same subject or predicate).
  */
-public class Releases {
-
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
-
+public interface TripleBunch extends JenaSet<Triple> {
+    /**
+     * Answer true iff this bunch is implemented as an array.
+     * This field is used to optimize some operations by avoiding the need for instanceOf tests.
+     *
+     * @return true iff this bunch is implemented as an arrays
+     */
+    boolean isArray();
 }
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/roaring/RoaringBitmapTripleIterator.java b/jena-core/src/main/java/org/apache/jena/mem2/store/roaring/RoaringBitmapTripleIterator.java
new file mode 100644
index 0000000000..d0fa825bc0
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/roaring/RoaringBitmapTripleIterator.java
@@ -0,0 +1,94 @@
+/*
+ * 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.jena.mem2.store.roaring;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.FastHashSet;
+import org.apache.jena.util.iterator.NiceIterator;
+import org.roaringbitmap.BatchIterator;
+import org.roaringbitmap.ImmutableBitmapDataProvider;
+
+import java.util.ConcurrentModificationException;
+import java.util.NoSuchElementException;
+import java.util.function.Consumer;
+
+/**
+ * A triple iterator that iterates over triple indices in a RoaringBitmap {@link BatchIterator}.
+ * Only the #forEachRemaining method uses {@link ImmutableBitmapDataProvider#forEach} if there has been no
+ * #next or #hasNext call before.
+ * All triples are stored in a {@link FastHashSet} and each triple is retrieved from the set by its index.
+ * The bitmap  typically is a subset of the triple indices in the set.
+ */
+public class RoaringBitmapTripleIterator extends NiceIterator<Triple> {
+    protected static final int BUFFER_SIZE = 64;
+    private final ImmutableBitmapDataProvider bitmap;
+    private final FastHashSet<Triple> triples;
+    private final int initialSize;
+    private final BatchIterator batchIterator;
+    private final int[] buffer = new int[BUFFER_SIZE];
+    private int bufferIndex = -1;
+    private boolean batchIteratorHasBeenUsed = false;
+
+    public RoaringBitmapTripleIterator(final ImmutableBitmapDataProvider bitmap, final FastHashSet<Triple> triples) {
+        this.bitmap = bitmap;
+        this.batchIterator = bitmap.getBatchIterator();
+        this.triples = triples;
+        this.initialSize = triples.size();
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (bufferIndex > 0)
+            return true;
+        if (this.batchIterator.hasNext()) {
+            if (!batchIteratorHasBeenUsed) {
+                batchIteratorHasBeenUsed = true;
+            }
+            bufferIndex = batchIterator.nextBatch(buffer);
+        }
+        return bufferIndex > 0;
+    }
+
+    @Override
+    public Triple next() {
+        if (triples.size() != initialSize) throw new ConcurrentModificationException();
+
+        if (this.hasNext())
+            return triples.getKeyAt(buffer[--bufferIndex]);
+
+        throw new NoSuchElementException();
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super Triple> action) {
+        if (batchIteratorHasBeenUsed) {
+            while (-1 < --bufferIndex) {
+                action.accept(triples.getKeyAt(buffer[bufferIndex]));
+            }
+            while (batchIterator.hasNext()) {
+                bufferIndex = batchIterator.nextBatch(buffer);
+                while (-1 < --bufferIndex) {
+                    action.accept(triples.getKeyAt(buffer[bufferIndex]));
+                }
+            }
+        } else {
+            bitmap.forEach((int index) -> action.accept(triples.getKeyAt(index)));
+        }
+        if (triples.size() != initialSize) throw new ConcurrentModificationException();
+    }
+}
diff --git a/jena-core/src/main/java/org/apache/jena/mem2/store/roaring/RoaringTripleStore.java b/jena-core/src/main/java/org/apache/jena/mem2/store/roaring/RoaringTripleStore.java
new file mode 100644
index 0000000000..547dd0a916
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/mem2/store/roaring/RoaringTripleStore.java
@@ -0,0 +1,335 @@
+/*
+ * 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.jena.mem2.store.roaring;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.collection.FastHashMap;
+import org.apache.jena.mem2.collection.FastHashSet;
+import org.apache.jena.mem2.pattern.MatchPattern;
+import org.apache.jena.mem2.pattern.PatternClassifier;
+import org.apache.jena.mem2.store.TripleStore;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
+import org.apache.jena.util.iterator.SingletonIterator;
+import org.roaringbitmap.FastAggregation;
+import org.roaringbitmap.ImmutableBitmapDataProvider;
+import org.roaringbitmap.RoaringBitmap;
+
+import java.util.stream.Stream;
+
+/**
+ * A triple store that is ideal for handling extremely large graphs.
+ * <p>
+ * Internal structure:
+ * - One indexed hash set (same as GraphMem2Fast uses) that holds all triples
+ * - Three hash maps indexed by subjects, predicates, and objects with RoaringBitmaps as values
+ * - The bitmaps contain the indices of the triples in the central hash set
+ * <p>
+ * The bitmaps are used to quickly find triples that match a given pattern.
+ * The bitmaps operations like {@link FastAggregation#naive_and(RoaringBitmap...)} and
+ * {@link RoaringBitmap#intersects(RoaringBitmap, RoaringBitmap)} are used to find matches for the pattern
+ * S_O, SP_, and _PO pretty fast, even in large graphs.
+ * <p>
+ * Additional optimizations:
+ * - because we know that if a triple exists in one of the maps, it also exists in the other two, we can use the
+ * {@link org.apache.jena.mem2.collection.JenaMapSetCommon#removeUnchecked(java.lang.Object)} method to avoid
+ * unnecessary checks.
+ */
+public class RoaringTripleStore implements TripleStore {
+
+    private static final String UNKNOWN_PATTERN_CLASSIFIER = "Unknown pattern classifier: %s";
+    private static final RoaringBitmap EMPTY_BITMAP = new RoaringBitmap();
+    final NodesToBitmapsMap subjectBitmaps = new NodesToBitmapsMap();
+    final NodesToBitmapsMap predicateBitmaps = new NodesToBitmapsMap();
+    final NodesToBitmapsMap objectBitmaps = new NodesToBitmapsMap();
+    final TripleSet triples = new TripleSet(); // We use a list here to maintain the order of triples
+
+    private static void addIndex(final NodesToBitmapsMap map, final Node node, final int index) {
+        final var bitmap = map.computeIfAbsent(node, RoaringBitmap::new);
+        bitmap.add(index);
+    }
+
+    private static void removeIndex(final NodesToBitmapsMap map, final Node node, final int index) {
+        final var bitmap = map.get(node);
+        bitmap.remove(index);
+        if (bitmap.isEmpty()) {
+            map.removeUnchecked(node);
+        }
+    }
+
+    @Override
+    public void add(final Triple triple) {
+        final var index = triples.addAndGetIndex(triple);
+        if (index < 0) { /*triple already exists*/
+            return;
+        }
+        addIndex(this.subjectBitmaps, triple.getSubject(), index);
+        addIndex(this.predicateBitmaps, triple.getPredicate(), index);
+        addIndex(this.objectBitmaps, triple.getObject(), index);
+    }
+
+    @Override
+    public void remove(final Triple triple) {
+        final var index = triples.removeAndGetIndex(triple);
+        if (index < 0) { /*triple does not exist*/
+            return;
+        }
+        removeIndex(this.subjectBitmaps, triple.getSubject(), index);
+        removeIndex(this.predicateBitmaps, triple.getPredicate(), index);
+        removeIndex(this.objectBitmaps, triple.getObject(), index);
+    }
+
+    @Override
+    public void clear() {
+        this.subjectBitmaps.clear();
+        this.predicateBitmaps.clear();
+        this.objectBitmaps.clear();
+        this.triples.clear();
+    }
+
+    @Override
+    public int countTriples() {
+        return this.triples.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return this.triples.isEmpty();
+    }
+
+    @Override
+    public boolean contains(Triple tripleMatch) {
+        final var matchPattern = PatternClassifier.classify(tripleMatch);
+        switch (matchPattern) {
+
+            case SUB_ANY_ANY:
+            case ANY_PRE_ANY:
+            case ANY_ANY_OBJ:
+            case SUB_PRE_ANY:
+            case ANY_PRE_OBJ:
+            case SUB_ANY_OBJ:
+                return hasMatchInBitmaps(tripleMatch, matchPattern);
+
+            case SUB_PRE_OBJ:
+                return this.triples.containsKey(tripleMatch);
+
+            case ANY_ANY_ANY:
+                return !this.isEmpty();
+
+            default:
+                throw new IllegalStateException(String.format(UNKNOWN_PATTERN_CLASSIFIER, PatternClassifier.classify(tripleMatch)));
+        }
+    }
+
+    private ImmutableBitmapDataProvider getBitmapForMatch(final Triple tripleMatch, final MatchPattern matchPattern) {
+        switch (matchPattern) {
+
+            case SUB_ANY_ANY:
+                return this.subjectBitmaps.getOrDefault(tripleMatch.getSubject(), EMPTY_BITMAP);
+            case ANY_PRE_ANY:
+                return this.predicateBitmaps.getOrDefault(tripleMatch.getPredicate(), EMPTY_BITMAP);
+            case ANY_ANY_OBJ:
+                return this.objectBitmaps.getOrDefault(tripleMatch.getObject(), EMPTY_BITMAP);
+
+            case SUB_PRE_ANY: {
+                final var subjectBitmap = this.subjectBitmaps.get(tripleMatch.getSubject());
+                if (null == subjectBitmap)
+                    return EMPTY_BITMAP;
+
+                final var predicateBitmap = this.predicateBitmaps.get(tripleMatch.getPredicate());
+                if (null == predicateBitmap)
+                    return EMPTY_BITMAP;
+
+                return FastAggregation.naive_and(subjectBitmap, predicateBitmap);
+            }
+
+            case ANY_PRE_OBJ: {
+                final var predicateBitmap = this.predicateBitmaps.get(tripleMatch.getPredicate());
+                if (null == predicateBitmap)
+                    return EMPTY_BITMAP;
+
+                final var objectBitmap = this.objectBitmaps.get(tripleMatch.getObject());
+                if (null == objectBitmap)
+                    return EMPTY_BITMAP;
+
+                return FastAggregation.naive_and(predicateBitmap, objectBitmap);
+            }
+
+            case SUB_ANY_OBJ: {
+                final var subjectBitmap = this.subjectBitmaps.get(tripleMatch.getSubject());
+                if (null == subjectBitmap)
+                    return EMPTY_BITMAP;
+
+                final var objectBitmap = this.objectBitmaps.get(tripleMatch.getObject());
+                if (null == objectBitmap)
+                    return EMPTY_BITMAP;
+
+                return FastAggregation.naive_and(subjectBitmap, objectBitmap);
+            }
+
+            case SUB_PRE_OBJ:
+                throw new IllegalArgumentException("Getting bitmap for match pattern SPO ist not supported because it is not efficient");
+
+            case ANY_ANY_ANY:
+                throw new IllegalArgumentException("Cannot get bitmap for match pattern ___");
+
+            default:
+                throw new IllegalStateException(String.format(UNKNOWN_PATTERN_CLASSIFIER, PatternClassifier.classify(tripleMatch)));
+        }
+    }
+
+    private boolean hasMatchInBitmaps(final Triple tripleMatch, final MatchPattern matchPattern) {
+        switch (matchPattern) {
+
+            case SUB_ANY_ANY:
+                return this.subjectBitmaps.containsKey(tripleMatch.getSubject());
+            case ANY_PRE_ANY:
+                return this.predicateBitmaps.containsKey(tripleMatch.getPredicate());
+            case ANY_ANY_OBJ:
+                return this.objectBitmaps.containsKey(tripleMatch.getObject());
+
+            case SUB_PRE_ANY: {
+                final var subjectBitmap = this.subjectBitmaps.get(tripleMatch.getSubject());
+                if (null == subjectBitmap)
+                    return false;
+
+                final var predicateBitmap = this.predicateBitmaps.get(tripleMatch.getPredicate());
+                if (null == predicateBitmap)
+                    return false;
+
+                return RoaringBitmap.intersects(subjectBitmap, predicateBitmap);
+            }
+
+            case ANY_PRE_OBJ: {
+                final var predicateBitmap = this.predicateBitmaps.get(tripleMatch.getPredicate());
+                if (null == predicateBitmap)
+                    return false;
+
+                final var objectBitmap = this.objectBitmaps.get(tripleMatch.getObject());
+                if (null == objectBitmap)
+                    return false;
+
+                return RoaringBitmap.intersects(objectBitmap, predicateBitmap);
+            }
+
+            case SUB_ANY_OBJ: {
+                final var subjectBitmap = this.subjectBitmaps.get(tripleMatch.getSubject());
+                if (null == subjectBitmap)
+                    return false;
+
+                final var objectBitmap = this.objectBitmaps.get(tripleMatch.getObject());
+                if (null == objectBitmap)
+                    return false;
+
+                return RoaringBitmap.intersects(subjectBitmap, objectBitmap);
+            }
+
+            case SUB_PRE_OBJ:
+                throw new IllegalArgumentException("Getting bitmap for match pattern SPO ist not supported because it is not efficient");
+
+            case ANY_ANY_ANY:
+                throw new IllegalArgumentException("Cannot get bitmap for match pattern ___");
+
+            default:
+                throw new IllegalStateException(String.format(UNKNOWN_PATTERN_CLASSIFIER, PatternClassifier.classify(tripleMatch)));
+        }
+    }
+
+    @Override
+    public Stream<Triple> stream() {
+        return this.triples.keyStream();
+    }
+
+    @Override
+    public Stream<Triple> stream(Triple tripleMatch) {
+        var pattern = PatternClassifier.classify(tripleMatch);
+        switch (pattern) {
+
+            case SUB_PRE_OBJ:
+                return this.triples.containsKey(tripleMatch) ? Stream.of(tripleMatch) : Stream.empty();
+
+            case SUB_PRE_ANY:
+            case SUB_ANY_OBJ:
+            case SUB_ANY_ANY:
+            case ANY_PRE_OBJ:
+            case ANY_PRE_ANY:
+            case ANY_ANY_OBJ:
+                return this.getBitmapForMatch(tripleMatch, pattern)
+                        .stream().mapToObj(this.triples::getKeyAt);
+
+            case ANY_ANY_ANY:
+                return this.stream();
+
+            default:
+                throw new IllegalStateException("Unknown pattern classifier: " + PatternClassifier.classify(tripleMatch));
+        }
+    }
+
+    @Override
+    public ExtendedIterator<Triple> find(Triple tripleMatch) {
+        var pattern = PatternClassifier.classify(tripleMatch);
+        switch (pattern) {
+
+            case SUB_PRE_OBJ:
+                return this.triples.containsKey(tripleMatch) ? new SingletonIterator<>(tripleMatch) : NiceIterator.emptyIterator();
+
+            case SUB_PRE_ANY:
+            case SUB_ANY_OBJ:
+            case SUB_ANY_ANY:
+            case ANY_PRE_OBJ:
+            case ANY_PRE_ANY:
+            case ANY_ANY_OBJ:
+                return new RoaringBitmapTripleIterator(this.getBitmapForMatch(tripleMatch, pattern), this.triples);
+
+            case ANY_ANY_ANY:
+                return this.triples.keyIterator();
+
+            default:
+                throw new IllegalStateException("Unknown pattern classifier: " + PatternClassifier.classify(tripleMatch));
+        }
+    }
+
+    /**
+     * Set of triples that is backed by a {@link TripleSet}.
+     */
+    private static class TripleSet extends FastHashSet<Triple> {
+
+        @Override
+        protected Triple[] newKeysArray(int size) {
+            return new Triple[size];
+        }
+    }
+
+    /**
+     * Map from {@link Node} to {@link RoaringBitmap}.
+     */
+    private static class NodesToBitmapsMap extends FastHashMap<Node, RoaringBitmap> {
+
+        @Override
+        protected Node[] newKeysArray(int size) {
+            return new Node[size];
+        }
+
+        @Override
+        protected RoaringBitmap[] newValuesArray(int size) {
+            return new RoaringBitmap[size];
+        }
+    }
+}
diff --git a/jena-core/src/test/java/org/apache/jena/graph/test/TestGraph.java b/jena-core/src/test/java/org/apache/jena/graph/test/TestGraph.java
index f650c2d474..5c25c2a3d2 100644
--- a/jena-core/src/test/java/org/apache/jena/graph/test/TestGraph.java
+++ b/jena-core/src/test/java/org/apache/jena/graph/test/TestGraph.java
@@ -23,12 +23,15 @@ package org.apache.jena.graph.test;
     and reifier test suites.
 */
 
-import junit.framework.Test ;
-import junit.framework.TestSuite ;
-import org.apache.jena.graph.GraphMemFactory ;
-import org.apache.jena.graph.Graph ;
-import org.apache.jena.graph.impl.WrappedGraph ;
-import org.apache.jena.mem.GraphMem ;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.GraphMemFactory;
+import org.apache.jena.graph.impl.WrappedGraph;
+import org.apache.jena.mem.GraphMem;
+import org.apache.jena.mem2.GraphMem2Fast;
+import org.apache.jena.mem2.GraphMem2Legacy;
+import org.apache.jena.mem2.GraphMem2Roaring;
 
 @SuppressWarnings("deprecation")
 public class TestGraph extends GraphTestBase
@@ -42,13 +45,19 @@ public class TestGraph extends GraphTestBase
      */
     public static TestSuite suite()
         {
-        TestSuite result = new TestSuite( TestGraph.class );
-        result.addTest( suite( MetaTestGraph.class, GraphMem.class ) );
-        result.addTest( suite( TestReifier.class, GraphMem.class ) );
-        result.addTest( suite( MetaTestGraph.class, WrappedGraphMem.class ) );
-        result.addTest( suite( TestReifier.class, WrappedGraphMem.class ) );
-        result.addTest( TestGraphListener.suite() );
-        result.addTestSuite( TestRegisterGraphListener.class );
+        TestSuite result = new TestSuite(TestGraph.class);
+        result.addTest(suite(MetaTestGraph.class, GraphMem.class));
+        result.addTest(suite(TestReifier.class, GraphMem.class));
+        result.addTest(suite(MetaTestGraph.class, WrappedGraphMem.class));
+        result.addTest(suite(TestReifier.class, WrappedGraphMem.class));
+        result.addTest(suite(MetaTestGraph.class, GraphMem2Fast.class));
+        result.addTest(suite(TestReifier.class, GraphMem2Fast.class));
+        result.addTest(suite(MetaTestGraph.class, GraphMem2Legacy.class));
+        result.addTest(suite(TestReifier.class, GraphMem2Legacy.class));
+        result.addTest(suite(MetaTestGraph.class, GraphMem2Roaring.class));
+        result.addTest(suite(TestReifier.class, GraphMem2Roaring.class));
+        result.addTest(TestGraphListener.suite());
+        result.addTestSuite(TestRegisterGraphListener.class);
         return result;
         }
 
diff --git a/jena-core/src/test/java/org/apache/jena/mem2/AbstractGraphMem2Test.java b/jena-core/src/test/java/org/apache/jena/mem2/AbstractGraphMem2Test.java
new file mode 100644
index 0000000000..a0ad17d208
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/mem2/AbstractGraphMem2Test.java
@@ -0,0 +1,1003 @@
+/*
+ * 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.jena.mem2;
+
+import org.apache.jena.datatypes.xsd.impl.XSDDouble;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.graph.Triple;
+import org.hamcrest.collection.IsEmptyCollection;
+import org.hamcrest.collection.IsIterableContainingInAnyOrder;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.stream.Collectors;
+
+import static org.apache.jena.testing_framework.GraphHelper.node;
+import static org.apache.jena.testing_framework.GraphHelper.triple;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.*;
+
+public abstract class AbstractGraphMem2Test {
+
+    protected Graph sut;
+
+    protected abstract Graph createGraph();
+
+    @Before
+    public void setUp() throws Exception {
+        sut = createGraph();
+    }
+
+    @Test
+    public void testClear() {
+        sut.add(triple("x R y"));
+        assertEquals(1, sut.size());
+        sut.clear();
+        assertEquals(0, sut.size());
+        assertTrue(sut.isEmpty());
+    }
+
+
+    @Test
+    public void testDelete() {
+        sut.add(triple("x R y"));
+        assertEquals(1, sut.size());
+        sut.delete(triple("x R y"));
+        assertEquals(0, sut.size());
+        assertTrue(sut.isEmpty());
+    }
+
+    @Test
+    public void testFind() {
+        sut.add(triple("x R y"));
+        assertEquals(1, sut.find(triple("x R y")).toList().size());
+        assertEquals(0, sut.find(triple("x R z")).toList().size());
+    }
+
+    @Test
+    public void testFind1() {
+        sut.add(triple("x R y"));
+        assertEquals(1, sut.find(null, null, null).toList().size());
+        assertEquals(1, sut.find(null, null, node("y")).toList().size());
+        assertEquals(1, sut.find(null, node("R"), null).toList().size());
+        assertEquals(1, sut.find(null, node("R"), node("y")).toList().size());
+        assertEquals(1, sut.find(node("x"), null, null).toList().size());
+        assertEquals(1, sut.find(node("x"), null, node("y")).toList().size());
+        assertEquals(1, sut.find(node("x"), node("R"), null).toList().size());
+        assertEquals(1, sut.find(node("x"), node("R"), node("y")).toList().size());
+    }
+
+    @Test
+    public void testFind2() {
+        sut.add(triple("x R y"));
+        assertEquals(1, sut.find(null, null, null).toList().size());
+        assertEquals(0, sut.find(null, null, node("z")).toList().size());
+        assertEquals(0, sut.find(null, node("S"), null).toList().size());
+        assertEquals(0, sut.find(null, node("S"), node("y")).toList().size());
+        assertEquals(0, sut.find(node("y"), null, null).toList().size());
+        assertEquals(0, sut.find(node("y"), null, node("y")).toList().size());
+        assertEquals(0, sut.find(node("y"), node("R"), null).toList().size());
+        assertEquals(0, sut.find(node("y"), node("R"), node("y")).toList().size());
+    }
+
+    @Test
+    public void testFindWithIteratorHasNextNext() {
+        sut.add(triple("x R y"));
+        var iter = sut.find(triple("x R y"));
+        assertTrue(iter.hasNext());
+        assertEquals(triple("x R y"), iter.next());
+        assertFalse(iter.hasNext());
+    }
+
+    @Test
+    public void testFindSPO() {
+        sut.add(triple("x R y"));
+        assertEquals(1, sut.find(node("x"), node("R"), node("y")).toList().size());
+        assertEquals(0, sut.find(node("x"), node("R"), node("z")).toList().size());
+    }
+
+    @Test
+    public void testFind___() {
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.find(null, null, null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc, bAa, bAb, bAc, cBa, cBb, cBc));
+    }
+
+    @Test
+    public void testFindS__() {
+        assertFalse(sut.find(node("a"), null, null).hasNext());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.find(node("a"), null, null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc));
+
+        findings = sut.find(node("b"), null, null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(bAa, bAb, bAc));
+
+        findings = sut.find(node("c"), null, null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa, cBb, cBc));
+
+        findings = sut.find(node("d"), null, null).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testFind_P_() {
+        assertFalse(sut.find(null, node("A"), null).hasNext());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.find(null, node("A"), null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc, bAa, bAb, bAc));
+
+        findings = sut.find(null, node("B"), null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa, cBb, cBc));
+
+        findings = sut.find(null, node("C"), null).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testFind__O() {
+        assertFalse(sut.find(null, null, node("a")).hasNext());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.find(null, null, node("a")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, bAa, cBa));
+
+        findings = sut.find(null, null, node("b")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAb, bAb, cBb));
+
+        findings = sut.find(null, null, node("c")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAc, bAc, cBc));
+
+        findings = sut.find(null, null, node("d")).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testFindSP_() {
+        assertFalse(sut.find(node("a"), node("A"), null).hasNext());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.find(node("a"), node("A"), null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc));
+
+        findings = sut.find(node("b"), node("A"), null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(bAa, bAb, bAc));
+
+        findings = sut.find(node("c"), node("B"), null).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa, cBb, cBc));
+
+        findings = sut.find(node("d"), node("C"), null).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.find(node("a"), node("B"), null).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testFindS_O() {
+        assertFalse(sut.find(node("a"), null, node("a")).hasNext());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.find(node("a"), null, node("a")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa));
+
+        findings = sut.find(node("b"), null, node("a")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(bAa));
+
+        findings = sut.find(node("c"), null, node("a")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa));
+
+        findings = sut.find(node("d"), null, node("a")).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.find(node("a"), null, node("d")).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testFind_PO() {
+        assertFalse(sut.find(null, node("A"), node("a")).hasNext());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.find(null, node("A"), node("a")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, bAa));
+
+        findings = sut.find(null, node("B"), node("a")).toList();
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa));
+
+        findings = sut.find(null, node("C"), node("a")).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.find(null, node("A"), node("d")).toList();
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testStream() {
+        sut.add(triple("x R y"));
+        assertEquals(1, sut.stream().count());
+    }
+
+    @Test
+    public void testStreamEmpty() {
+        assertEquals(0, sut.stream().count());
+    }
+
+    @Test
+    public void testStreamSPO() {
+        assertEquals(0, sut.stream(node("x"), node("R"), node("y")).count());
+
+        var t = triple("x R y");
+        sut.add(t);
+        var findings = sut.stream(t.getSubject(), t.getPredicate(), t.getObject()).collect(Collectors.toList());
+        assertEquals(1, findings.size());
+        assertEquals(findings.get(0), t);
+    }
+
+    @Test
+    public void testStream___() {
+        assertEquals(0, sut.stream(null, null, null).count());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.stream(null, null, null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc, bAa, bAb, bAc, cBa, cBb, cBc));
+    }
+
+    @Test
+    public void testStreamS__() {
+        assertEquals(0, sut.stream(node("a"), null, null).count());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.stream(aAa.getSubject(), null, null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc));
+
+        findings = sut.stream(bAa.getSubject(), null, null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(bAa, bAb, bAc));
+
+        findings = sut.stream(cBa.getSubject(), null, null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa, cBb, cBc));
+
+        findings = sut.stream(node("d"), null, null).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testStream_P_() {
+        assertEquals(0, sut.stream(null, node("A"), null).count());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.stream(null, aAa.getPredicate(), null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc, bAa, bAb, bAc));
+
+        findings = sut.stream(null, cBa.getPredicate(), null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa, cBb, cBc));
+
+        findings = sut.stream(null, node("C"), null).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testStream__O() {
+        assertEquals(0, sut.stream(null, null, node("a")).count());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.stream(null, null, aAa.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, bAa, cBa));
+
+        findings = sut.stream(null, null, aAb.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAb, bAb, cBb));
+
+        findings = sut.stream(null, null, aAc.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAc, bAc, cBc));
+
+        findings = sut.stream(null, null, node("d")).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testStreamSP_() {
+        assertEquals(0, sut.stream(node("a"), node("A"), null).count());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.stream(aAa.getSubject(), aAa.getPredicate(), null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, aAb, aAc));
+
+        findings = sut.stream(bAa.getSubject(), bAa.getPredicate(), null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(bAa, bAb, bAc));
+
+        findings = sut.stream(cBa.getSubject(), cBa.getPredicate(), null).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa, cBb, cBc));
+
+        findings = sut.stream(node("a"), node("C"), null).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.stream(node("d"), node("D"), null).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testStreamS_O() {
+        assertEquals(0, sut.stream(node("a"), null, node("a")).count());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.stream(aAa.getSubject(), null, aAa.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa));
+
+        findings = sut.stream(bAa.getSubject(), null, bAa.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(bAa));
+
+        findings = sut.stream(cBa.getSubject(), null, cBa.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa));
+
+        findings = sut.stream(node("d"), null, node("d")).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.stream(node("d"), null, node("a")).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.stream(node("a"), null, node("d")).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testStream_PO() {
+        assertEquals(0, sut.stream(null, node("A"), node("a")).count());
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        var findings = sut.stream(null, aAa.getPredicate(), aAa.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, bAa));
+
+        findings = sut.stream(null, bAa.getPredicate(), bAa.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(aAa, bAa));
+
+        findings = sut.stream(null, cBa.getPredicate(), cBa.getObject()).collect(Collectors.toList());
+        assertThat(findings, IsIterableContainingInAnyOrder.containsInAnyOrder(cBa));
+
+        findings = sut.stream(null, node("C"), node("a")).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.stream(null, node("A"), node("d")).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+
+        findings = sut.stream(null, node("D"), node("d")).collect(Collectors.toList());
+        assertThat(findings, IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testContains() {
+        sut.add(triple("x R y"));
+        assertTrue(sut.contains(triple("x R y")));
+        assertFalse(sut.contains(triple("x R z")));
+    }
+
+    @Test
+    public void testContains1() {
+        sut.add(triple("x R y"));
+        sut.add(triple("y S z"));
+        sut.add(triple("z T a"));
+        assertTrue(sut.contains(null, null, null));
+        assertTrue(sut.contains(null, null, node("y")));
+        assertTrue(sut.contains(null, node("R"), null));
+        assertTrue(sut.contains(null, node("R"), node("y")));
+        assertTrue(sut.contains(node("x"), null, null));
+        assertTrue(sut.contains(node("x"), null, node("y")));
+        assertTrue(sut.contains(node("x"), node("R"), null));
+        assertTrue(sut.contains(node("x"), node("R"), node("y")));
+    }
+
+    @Test
+    public void testContains2() {
+        sut.add(triple("x R y"));
+        sut.add(triple("y S z"));
+        sut.add(triple("z T a"));
+        assertTrue(sut.contains(null, null, null));
+        assertFalse(sut.contains(null, null, node("x")));
+        assertFalse(sut.contains(null, node("U"), null));
+        assertFalse(sut.contains(null, node("R"), node("z")));
+        assertFalse(sut.contains(node("a"), null, null));
+        assertFalse(sut.contains(node("x"), null, node("x")));
+        assertFalse(sut.contains(node("y"), node("R"), null));
+        assertFalse(sut.contains(node("y"), node("T"), node("a")));
+    }
+
+    @Test
+    public void testContainsSPO() {
+        assertFalse(sut.contains(node("a"), node("A"), node("a")));
+
+        var t = triple("a A a");
+        sut.add(t);
+        assertTrue(sut.contains(t.getSubject(), t.getPredicate(), t.getObject()));
+        assertFalse(sut.contains(t.getSubject(), t.getPredicate(), node("b")));
+        assertFalse(sut.contains(t.getSubject(), node("B"), t.getObject()));
+        assertFalse(sut.contains(node("b"), t.getPredicate(), t.getObject()));
+    }
+
+    @Test
+    public void testContains___() {
+        assertFalse(sut.contains(null, null, null));
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        assertTrue(sut.contains(null, null, null));
+    }
+
+    @Test
+    public void testContainsS__() {
+        assertFalse(sut.contains(node("a"), null, null));
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        assertTrue(sut.contains(aAa.getSubject(), null, null));
+
+        assertTrue(sut.contains(bAa.getSubject(), null, null));
+
+        assertTrue(sut.contains(cBa.getSubject(), null, null));
+
+        assertFalse(sut.contains(node("d"), null, null));
+    }
+
+    @Test
+    public void testContains_P_() {
+        assertFalse(sut.contains(null, node("A"), null));
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        assertTrue(sut.contains(null, aAa.getPredicate(), null));
+
+        assertTrue(sut.contains(null, cBa.getPredicate(), null));
+
+        assertFalse(sut.contains(null, node("C"), null));
+    }
+
+    @Test
+    public void testContains__O() {
+        assertFalse(sut.contains(null, null, node("a")));
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        assertTrue(sut.contains(null, null, aAa.getObject()));
+
+        assertTrue(sut.contains(null, null, aAb.getObject()));
+
+        assertTrue(sut.contains(null, null, aAc.getObject()));
+
+        assertFalse(sut.contains(null, null, node("d")));
+    }
+
+    @Test
+    public void testContainsSP_() {
+        assertFalse(sut.contains(node("a"), node("A"), null));
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        assertTrue(sut.contains(aAa.getSubject(), aAa.getPredicate(), null));
+
+        assertTrue(sut.contains(bAa.getSubject(), bAa.getPredicate(), null));
+
+        assertTrue(sut.contains(cBa.getSubject(), cBa.getPredicate(), null));
+
+        assertFalse(sut.contains(node("a"), node("C"), null));
+
+        assertFalse(sut.contains(node("d"), node("D"), null));
+    }
+
+    @Test
+    public void testContainsS_O() {
+        assertFalse(sut.contains(node("a"), null, node("a")));
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        assertTrue(sut.contains(aAa.getSubject(), null, aAa.getObject()));
+
+        assertTrue(sut.contains(bAa.getSubject(), null, bAa.getObject()));
+
+        assertTrue(sut.contains(cBa.getSubject(), null, cBa.getObject()));
+
+        assertFalse(sut.contains(node("d"), null, node("d")));
+
+        assertFalse(sut.contains(node("d"), null, node("a")));
+
+        assertFalse(sut.contains(node("a"), null, node("d")));
+    }
+
+    @Test
+    public void testContains_PO() {
+        assertFalse(sut.contains(null, node("A"), node("a")));
+
+        final var aAa = triple("a A a");
+        final var aAb = triple("a A b");
+        final var aAc = triple("a A c");
+        final var bAa = triple("b A a");
+        final var bAb = triple("b A b");
+        final var bAc = triple("b A c");
+        final var cBa = triple("c B a");
+        final var cBb = triple("c B b");
+        final var cBc = triple("c B c");
+
+        sut.add(aAa);
+        sut.add(aAb);
+        sut.add(aAc);
+        sut.add(bAa);
+        sut.add(bAb);
+        sut.add(bAc);
+        sut.add(cBa);
+        sut.add(cBb);
+        sut.add(cBc);
+
+        assertTrue(sut.contains(null, aAa.getPredicate(), aAa.getObject()));
+
+        assertTrue(sut.contains(null, bAa.getPredicate(), bAa.getObject()));
+
+        assertTrue(sut.contains(null, cBa.getPredicate(), cBa.getObject()));
+
+        assertFalse(sut.contains(null, node("C"), node("a")));
+
+        assertFalse(sut.contains(null, node("A"), node("d")));
+
+        assertFalse(sut.contains(null, node("D"), node("d")));
+    }
+
+    @Test
+    public void testContainsValueObject() {
+        sut.add(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"),
+                NodeFactory.createLiteral("0.1", XSDDouble.XSDdouble)));
+        assertTrue(sut.contains(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"),
+                NodeFactory.createLiteral("0.1", XSDDouble.XSDdouble))));
+        assertFalse(sut.contains(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"),
+                NodeFactory.createLiteral("0.10", XSDDouble.XSDdouble))));
+        assertFalse(sut.contains(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"),
+                NodeFactory.createLiteral("0.11", XSDDouble.XSDdouble))));
+    }
+
+    @Test
+    public void testContainsValueSubject() {
+        var containedTriple = Triple.create(
+                NodeFactory.createLiteral("0.1", XSDDouble.XSDdouble),
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"));
+        sut.add(containedTriple);
+
+        var match = Triple.create(
+                NodeFactory.createLiteral("0.1", XSDDouble.XSDdouble),
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"));
+        assertTrue(sut.contains(match));
+        assertEquals(containedTriple, sut.find(match).next());
+
+        match = Triple.create(
+                NodeFactory.createLiteral("0.10", XSDDouble.XSDdouble),
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"));
+        assertFalse(sut.contains(match));
+        assertFalse(sut.find(match).hasNext());
+
+        match = Triple.create(
+                NodeFactory.createLiteral("0.11", XSDDouble.XSDdouble),
+                NodeFactory.createURI("x"),
+                NodeFactory.createURI("R"));
+        assertFalse(sut.contains(match));
+        assertFalse(sut.find(match).hasNext());
+    }
+
+    @Test
+    public void testContainsValuePredicate() {
+        sut.add(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createLiteral("0.1", XSDDouble.XSDdouble),
+                NodeFactory.createURI("R")));
+        assertTrue(sut.contains(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createLiteral("0.1", XSDDouble.XSDdouble),
+                NodeFactory.createURI("R"))));
+        assertFalse(sut.contains(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createLiteral("0.10", XSDDouble.XSDdouble),
+                NodeFactory.createURI("R"))));
+        assertFalse(sut.contains(Triple.create(
+                NodeFactory.createURI("x"),
+                NodeFactory.createLiteral("0.11", XSDDouble.XSDdouble),
+                NodeFactory.createURI("R"))));
+    }
+
+}
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2FastTest.java
similarity index 60%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/test/java/org/apache/jena/mem2/GraphMem2FastTest.java
index f601955053..2a03057c62 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2FastTest.java
@@ -15,18 +15,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
 
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
+package org.apache.jena.mem2;
 
-/**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
- */
-public class Releases {
+import org.apache.jena.graph.Graph;
 
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+public class GraphMem2FastTest extends AbstractGraphMem2Test {
 
-}
+    @Override
+    protected Graph createGraph() {
+        return new GraphMem2Fast();
+    }
+}
\ No newline at end of file
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2LegacyTest.java
similarity index 60%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/test/java/org/apache/jena/mem2/GraphMem2LegacyTest.java
index f601955053..2f2944a096 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2LegacyTest.java
@@ -15,18 +15,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2;
 
 import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
 
-/**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
- */
-public class Releases {
-
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+public class GraphMem2LegacyTest extends AbstractGraphMem2Test {
 
-}
+    @Override
+    protected Graph createGraph() {
+        return new GraphMem2Legacy();
+    }
+}
\ No newline at end of file
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2RoaringTest.java
similarity index 60%
copy from jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
copy to jena-core/src/test/java/org/apache/jena/mem2/GraphMem2RoaringTest.java
index f601955053..bc539bab1a 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/mem/graph/helper/Releases.java
+++ b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2RoaringTest.java
@@ -15,18 +15,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jena.mem.graph.helper;
+package org.apache.jena.mem2;
 
 import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
 
-/**
- * Proviced the release specific access to the helper classes for working with graphs, triples and nodes.
- */
-public class Releases {
-
-   public static GraphTripleNodeHelper<Graph, Triple, Node> current = new GraphTripleNodeHelperCurrent();
-   public static GraphTripleNodeHelper<org.apache.shadedJena480.graph.Graph, org.apache.shadedJena480.graph.Triple, org.apache.shadedJena480.graph.Node> v480 = new GraphTripleNodeHelper480();
+public class GraphMem2RoaringTest extends AbstractGraphMem2Test {
 
-}
+    @Override
+    protected Graph createGraph() {
+        return new GraphMem2Roaring();
+    }
+}
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2Test.java b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2Test.java
new file mode 100644
index 0000000000..aabb0c3c6a
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/mem2/GraphMem2Test.java
@@ -0,0 +1,173 @@
+/*
+ * 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.jena.mem2;
+
+import org.apache.jena.graph.Triple;
+import org.apache.jena.mem2.store.TripleStore;
+import org.apache.jena.util.iterator.NullIterator;
+import org.junit.Test;
+
+import java.util.stream.Stream;
+
+import static org.apache.jena.testing_framework.GraphHelper.node;
+import static org.apache.jena.testing_framework.GraphHelper.triple;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.*;
+
+public class GraphMem2Test {
+
+    @Test
+    public void destroy() {
+        TripleStore mockStore = mock();
+
+        var sut = new GraphMem2(mockStore);
+        sut.destroy();
+
+        inOrder(mockStore).verify(mockStore, times(1)).clear();
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void clear() {
+        TripleStore mockStore = mock();
+
+        when(mockStore.find(any())).thenReturn(NullIterator.emptyIterator());
+
+        var sut = new GraphMem2(mockStore);
+        sut.clear();
+
+        inOrder(mockStore).verify(mockStore, times(1)).find(any());
+        inOrder(mockStore).verify(mockStore, times(1)).clear();
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void performAdd() {
+        TripleStore mockStore = mock();
+
+        var triple = triple("a b x");
+
+        var sut = new GraphMem2(mockStore);
+        sut.performAdd(triple);
+
+        inOrder(mockStore).verify(mockStore, times(1)).add(triple);
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void performDelete() {
+        TripleStore mockStore = mock();
+
+        var triple = triple("a b x");
+
+        var sut = new GraphMem2(mockStore);
+        sut.performDelete(triple);
+
+        inOrder(mockStore).verify(mockStore, times(1)).remove(triple);
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void stream() {
+        TripleStore mockStore = mock();
+
+        when(mockStore.stream()).thenReturn(Stream.empty());
+
+        var sut = new GraphMem2(mockStore);
+        sut.stream();
+
+        inOrder(mockStore).verify(mockStore, times(1)).stream();
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void StreamMatch() {
+        TripleStore mockStore = mock();
+
+        var s = node("s");
+        var p = node("p");
+        var o = node("o");
+        var triple = Triple.create(s, p, o);
+
+        when(mockStore.stream(triple)).thenReturn(Stream.of(triple));
+
+        var sut = new GraphMem2(mockStore);
+        sut.stream(s, p, o);
+
+        inOrder(mockStore).verify(mockStore, times(1)).stream(triple);
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void graphBaseFind() {
+        TripleStore mockStore = mock();
+
+        var triple = triple("a b x");
+
+        when(mockStore.find(triple)).thenReturn(NullIterator.emptyIterator());
+
+        var sut = new GraphMem2(mockStore);
+        sut.graphBaseFind(triple);
+
+        inOrder(mockStore).verify(mockStore, times(1)).find(triple);
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void graphBaseContains() {
+        TripleStore mockStore = mock();
+
+        var triple = triple("a b x");
+
+        when(mockStore.contains(triple)).thenReturn(false);
+
+        var sut = new GraphMem2(mockStore);
+        sut.graphBaseContains(triple);
+
+        inOrder(mockStore).verify(mockStore, times(1)).contains(triple);
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void graphBaseSize() {
+        TripleStore mockStore = mock();
+
+        when(mockStore.countTriples()).thenReturn(0);
+
+        var sut = new GraphMem2(mockStore);
+        sut.graphBaseSize();
+
+        inOrder(mockStore).verify(mockStore, times(1)).countTriples();
+        verifyNoMoreInteractions(mockStore);
+    }
+
+    @Test
+    public void testGetCapabilities() {
+        TripleStore mockStore = mock();
+
+        var sut = new GraphMem2(mockStore);
+        var capapbilities = sut.getCapabilities();
+
+        assertTrue(capapbilities.sizeAccurate());
+        assertTrue(capapbilities.addAllowed());
+        assertTrue(capapbilities.deleteAllowed());
+        assertFalse(capapbilities.handlesLiteralTyping());
+
+    }
+}
\ No newline at end of file
diff --git a/jena-core/src/test/java/org/apache/jena/mem2/collection/AbstractJenaMapNodeTest.java b/jena-core/src/test/java/org/apache/jena/mem2/collection/AbstractJenaMapNodeTest.java
new file mode 100644
index 0000000000..69ba1bc9f9
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/mem2/collection/AbstractJenaMapNodeTest.java
@@ -0,0 +1,648 @@
+/*
+ * 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.jena.mem2.collection;
+
+import org.apache.jena.graph.Node;
+import org.hamcrest.collection.IsEmptyCollection;
+import org.hamcrest.collection.IsIterableContainingInAnyOrder;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static org.apache.jena.testing_framework.GraphHelper.node;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.*;
+
+public abstract class AbstractJenaMapNodeTest {
+
+    protected JenaMap<Node, Object> sut;
+
+    protected abstract JenaMap<Node, Object> createNodeMap();
+
+    @Before
+    public void setUp() throws Exception {
+        sut = createNodeMap();
+    }
+
+    @Test
+    public void isEmpty() {
+        assertTrue(sut.isEmpty());
+        sut.tryPut(node("s"), null);
+        assertFalse(sut.isEmpty());
+    }
+
+    @Test
+    public void testTryPut() {
+        assertTrue(sut.tryPut(node("s"), 1));
+        assertEquals(1, sut.size());
+        assertFalse(sut.tryPut(node("s"), 2));
+        assertEquals(1, sut.size());
+    }
+
+    @Test
+    public void testTryPutOverridesValue() {
+        sut.tryPut(node("s"), 1);
+        assertEquals(1, sut.get(node("s")));
+        sut.tryPut(node("s"), 2);
+        assertEquals(2, sut.get(node("s")));
+    }
+
+    @Test
+    public void testPut() {
+        sut.put(node("s"), 1);
+        assertEquals(1, sut.size());
+        sut.put(node("s"), 2);
+        assertEquals(1, sut.size());
+    }
+
+    @Test
+    public void testPutOverridesValue() {
+        sut.put(node("s"), 1);
+        assertEquals(1, sut.get(node("s")));
+        sut.put(node("s"), 2);
+        assertEquals(2, sut.get(node("s")));
+    }
+
+    @Test
+    public void testTryRemove() {
+        sut.put(node("s"), null);
+        assertTrue(sut.tryRemove(node("s")));
+        assertEquals(0, sut.size());
+        assertFalse(sut.tryRemove(node("s")));
+    }
+
+    @Test
+    public void testRemoveUnchecked() {
+        sut.put(node("s"), null);
+        sut.removeUnchecked(node("s"));
+        assertEquals(0, sut.size());
+    }
+
+    @Test
+    public void testGet() {
+        sut.put(node("s"), 1);
+        assertEquals(1, sut.get(node("s")));
+        assertNull(sut.get(node("s2")));
+    }
+
+    @Test
+    public void testGet2() {
+        sut.put(node("s"), 1);
+        sut.put(node("s2"), 2);
+        assertEquals(1, sut.get(node("s")));
+        assertEquals(2, sut.get(node("s2")));
+    }
+
+    @Test
+    public void testGetOrDefault() {
+        sut.put(node("s"), 1);
+        assertEquals(1, sut.getOrDefault(node("s"), 2));
+        assertEquals(2, sut.getOrDefault(node("s2"), 2));
+    }
+
+    @Test
+    public void testComputeIfAbsent() {
+        sut.computeIfAbsent(node("s"), () -> 1);
+        assertEquals(1, sut.get(node("s")));
+        sut.computeIfAbsent(node("s"), () -> 2);
+        assertEquals(1, sut.get(node("s")));
+    }
+
+    @Test
+    public void testCompute() {
+        sut.compute(node("s"), (v) -> {
+            assertNull(v);
+            return 1;
+        });
+        assertEquals(1, sut.get(node("s")));
+        sut.compute(node("s"), (v) -> {
+            assertEquals(1, v);
+            return 2;
+        });
+        assertEquals(2, sut.get(node("s")));
+        sut.compute(node("s"), (v) -> {
+            assertEquals(2, v);
+            return null;
+        });
+        assertNull(sut.get(node("s")));
+        assertTrue(sut.isEmpty());
+    }
+
+    @Test
+    public void testClear() {
+        sut.put(node("s"), null);
+        sut.clear();
+        assertEquals(0, sut.size());
+    }
+
+    @Test
+    public void testContainKey() {
+        assertFalse(sut.containsKey(node("s")));
+        sut.put(node("s"), null);
+        assertTrue(sut.containsKey(node("s")));
+        assertFalse(sut.containsKey(node("s2")));
+    }
+
+    @Test
+    public void testKeyIteratorEmpty() {
+        var iter = sut.keyIterator();
+        assertFalse(iter.hasNext());
+        assertThrows(NoSuchElementException.class, () -> iter.next());
+    }
+
+    @Test
+    public void testValueIteratorEmpty2() {
+        var iter = sut.valueIterator();
+        assertFalse(iter.hasNext());
+        assertThrows(NoSuchElementException.class, () -> iter.next());
+    }
+
+    @Test
+    public void testKeyIteratorNextThrowsConcurrentModificationException() {
+        sut.put(node("s"), null);
+        var iter = sut.keyIterator();
+        sut.put(node("s2"), null);
+        assertThrows(ConcurrentModificationException.class, iter::next);
+    }
+
+    @Test
+    public void testKeySpliteratorAdvanceThrowsConcurrentModificationException() {
+        sut.put(node("s"), null);
+        var spliterator = sut.keySpliterator();
+        sut.put(node("s2"), null);
+        assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> {
+        }));
+    }
+
+    @Test
+    public void testValueIteratorNextThrowsConcurrentModificationException() {
+        sut.put(node("s"), null);
+        var iter = sut.keyIterator();
+        sut.put(node("s2"), null);
+        assertThrows(ConcurrentModificationException.class, iter::next);
+    }
+
+    @Test
+    public void testValueSpliteratorAdvanceThrowsConcurrentModificationException() {
+        sut.put(node("s"), null);
+        var spliterator = sut.valueSpliterator();
+        sut.put(node("s2"), null);
+        assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(t -> {
+        }));
+    }
+
+    @Test
+    public void testKeyIterator1() {
+        final var n0 = node("s");
+
+        sut.put(n0, null);
+
+        final var iter = sut.keyIterator();
+        assertThat(iter.toList(), IsIterableContainingInAnyOrder.containsInAnyOrder(n0));
+    }
+
+    @Test
+    public void testValueIterator1() {
+        final var n0 = node("s");
+
+        sut.put(n0, 1);
+
+        final var iter = sut.valueIterator();
+        assertThat(iter.toList(), IsIterableContainingInAnyOrder.containsInAnyOrder(1));
+
+    }
+
+    @Test
+    public void testKeyIterator2() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+
+        sut.put(n0, null);
+        sut.put(n1, null);
+
+        final var iter = sut.keyIterator();
+        assertThat(iter.toList(), IsIterableContainingInAnyOrder.containsInAnyOrder(n0, n1));
+    }
+
+    @Test
+    public void testValueIterator2() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+
+        sut.put(n0, 1);
+        sut.put(n1, 2);
+
+        final var iter = sut.valueIterator();
+        assertThat(iter.toList(), IsIterableContainingInAnyOrder.containsInAnyOrder(1, 2));
+    }
+
+    @Test
+    public void testKeyIterator3() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+        final var n2 = node("s3");
+
+        sut.put(n0, null);
+        sut.put(n1, null);
+        sut.put(n2, null);
+
+        final var iter = sut.keyIterator();
+        assertThat(iter.toList(), IsIterableContainingInAnyOrder.containsInAnyOrder(n0, n1, n2));
+    }
+
+    @Test
+    public void testValueIterator3() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+        final var n2 = node("s3");
+
+        sut.put(n0, 1);
+        sut.put(n1, 2);
+        sut.put(n2, 3);
+
+        final var iter = sut.valueIterator();
+        assertThat(iter.toList(), IsIterableContainingInAnyOrder.containsInAnyOrder(1, 2, 3));
+    }
+
+    @Test
+    public void testKeySpliteratorEmpty() {
+        var spliterator = sut.keySpliterator();
+        assertFalse(spliterator.tryAdvance(t -> fail()));
+    }
+
+    @Test
+    public void testValueSpliteratorEmpty() {
+        var spliterator = sut.valueSpliterator();
+        assertFalse(spliterator.tryAdvance(t -> fail()));
+    }
+
+    @Test
+    public void testKeySpliterator1() {
+        final var n0 = node("s");
+
+        sut.put(n0, null);
+
+        final var spliterator = sut.keySpliterator();
+        final var list = StreamSupport.stream(spliterator, false).collect(Collectors.toList());
+        assertThat(list, IsIterableContainingInAnyOrder.containsInAnyOrder(n0));
+    }
+
+    @Test
+    public void testValueSpliterator1() {
+        sut.put(node("s"), 1);
+
+        final var spliterator = sut.valueSpliterator();
+        final var list = StreamSupport.stream(spliterator, false).collect(Collectors.toList());
+        assertThat(list, IsIterableContainingInAnyOrder.containsInAnyOrder(1));
+    }
+
+    @Test
+    public void testKeySpliterator2() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+
+        sut.put(n0, null);
+        sut.put(n1, null);
+
+        final var spliterator = sut.keySpliterator();
+        final var list = StreamSupport.stream(spliterator, false).collect(Collectors.toList());
+        assertThat(list, IsIterableContainingInAnyOrder.containsInAnyOrder(n0, n1));
+    }
+
+    @Test
+    public void testValueSpliterator2() {
+        sut.put(node("s"), 1);
+        sut.put(node("s2"), 2);
+
+        final var spliterator = sut.valueSpliterator();
+        final var list = StreamSupport.stream(spliterator, false).collect(Collectors.toList());
+        assertThat(list, IsIterableContainingInAnyOrder.containsInAnyOrder(1, 2));
+    }
+
+    @Test
+    public void testKeySpliterator3() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+        final var n2 = node("s3");
+
+        sut.put(n0, null);
+        sut.put(n1, null);
+        sut.put(n2, null);
+
+        final var spliterator = sut.keySpliterator();
+        final var list = StreamSupport.stream(spliterator, false).collect(Collectors.toList());
+        assertThat(list, IsIterableContainingInAnyOrder.containsInAnyOrder(n0, n1, n2));
+    }
+
+    @Test
+    public void testValueSpliterator3() {
+        sut.put(node("s"), 1);
+        sut.put(node("s2"), 2);
+        sut.put(node("s3"), 3);
+
+        final var spliterator = sut.valueSpliterator();
+        final var list = StreamSupport.stream(spliterator, false).collect(Collectors.toList());
+        assertThat(list, IsIterableContainingInAnyOrder.containsInAnyOrder(1, 2, 3));
+    }
+
+    @Test
+    public void testKeyStreamEmpty() {
+        var stream = sut.keyStream();
+        assertThat(stream.collect(Collectors.toList()), IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testValueStreamEmpty() {
+        var stream = sut.valueStream();
+        assertThat(stream.collect(Collectors.toList()), IsEmptyCollection.empty());
+    }
+
+    @Test
+    public void testKeyStream1() {
+        final var n0 = node("s");
+
+        sut.put(n0, null);
+
+        final var stream = sut.keyStream();
+        assertThat(stream.collect(Collectors.toList()), IsIterableContainingInAnyOrder.containsInAnyOrder(n0));
+    }
+
+    @Test
+    public void testValueStream1() {
+        sut.put(node("s"), 1);
+
+        final var stream = sut.valueStream();
+        assertThat(stream.collect(Collectors.toList()), IsIterableContainingInAnyOrder.containsInAnyOrder(1));
+    }
+
+    @Test
+    public void testKeyStream2() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+
+        sut.put(n0, null);
+        sut.put(n1, null);
+
+        final var stream = sut.keyStream();
+        assertThat(stream.collect(Collectors.toList()), IsIterableContainingInAnyOrder.containsInAnyOrder(n0, n1));
+    }
+
+    @Test
+    public void testValueStream2() {
+        sut.put(node("s"), 1);
+        sut.put(node("s2"), 2);
+
+        final var stream = sut.valueStream();
+        assertThat(stream.collect(Collectors.toList()), IsIterableContainingInAnyOrder.containsInAnyOrder(1, 2));
+    }
+
+    @Test
+    public void testKeyStream3() {
+        final var n0 = node("s");
+        final var n1 = node("s2");
+        final var n2 = node("s3");
+
+        sut.put(n0, null);
+        sut.put(n1, null);
+        sut.put(n2, null);
+
+        final var stream = sut.keyStream();
+        assertThat(stream.collect(Collectors.toList()), IsIterableContainingInAnyOrder.containsInAnyOrder(n0, n1, n2));
+    }
+
+    @Test
+    public void testValueStream3() {
... 4765 lines suppressed ...