diff --git a/.travis.yml b/.travis.yml
index 663b0b1fa9bc5267a5a9040ea9a170ca028829b5..d252183c6765df4a8c63d46cdecfe99f82f5f3ec 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,5 @@
 language: python
+
 python:
   - 2.7
   - 3.5
@@ -8,6 +9,13 @@ python:
   - pypy
   - pypy3
 
+jobs:
+  include:
+    # Special Linux builds
+    - name: "Python: 3.8, pure (no C extensions)"
+      python: 3.8
+      env: PURE_PYTHON=1
+
 install:
     - pip install -U pip
     - pip install -U setuptools zc.buildout
diff --git a/src/ZODB/tests/testCache.py b/src/ZODB/tests/testCache.py
index 50cf1debfab44230157bfe947fa66c774a1665f0..00ee696ca1cc0f203833331ab1853d6a80d33e76 100644
--- a/src/ZODB/tests/testCache.py
+++ b/src/ZODB/tests/testCache.py
@@ -383,6 +383,13 @@ class CacheErrors(unittest.TestCase):
             # to None actually go *down* by a few. Possibly it has to
             # do with the lazy tracking of frames?
             # (https://github.com/python/cpython/commit/5a625d0aa6a6d9ec6574ee8344b41d63dcb9897e)
+            #
+            # Likewise, on 3.8 with PURE_PYTHON it sometimes increases
+            # by 1; this is cleared up by a garbage collection (it's
+            # not clear where/why)
+            new_nones = rc(None)
+            if new_nones > nones:
+                gc.collect()
             self.assertLessEqual(rc(None), nones)
 
     def testTwoCaches(self):
diff --git a/src/ZODB/tests/testConnectionSavepoint.py b/src/ZODB/tests/testConnectionSavepoint.py
index a82651d34d7012ff93f0f49047232ef9c8e7676f..477a9559d35c924bd7456db8dce211dbe69596dd 100644
--- a/src/ZODB/tests/testConnectionSavepoint.py
+++ b/src/ZODB/tests/testConnectionSavepoint.py
@@ -127,17 +127,11 @@ then, + 1 for the root object:
     True
 
 Making a savepoint at this time used to leave the cache holding the same
-number of objects.  Make sure the cache shrinks now instead.
+number of objects. Make sure the cache shrinks now instead. (Implementations that use
+weak references, such as PyPy, may need a garbage collection.)
 
     >>> dummy = transaction.savepoint()
-
-Jython needs a GC, and needs to actually access the cache data to be
-sure the size is updated (it uses "eventually consistent" implementations for
-its weak dictionaries):
-
     >>> _ = gc.collect()
-    >>> _ = getattr(cn._cache, 'data', {}).values()
-    >>> _ = getattr(cn._cache, 'data', {}).keys()
     >>> len(cn._cache) <= CACHESIZE + 1
     True
 
diff --git a/src/ZODB/tests/testThreadedShutdown.py b/src/ZODB/tests/testThreadedShutdown.py
index 12d666d9b6d057de3cb6257589b3d470d8745abc..030581056ee1587f51943cf2afd39dccf06f1d02 100644
--- a/src/ZODB/tests/testThreadedShutdown.py
+++ b/src/ZODB/tests/testThreadedShutdown.py
@@ -7,7 +7,7 @@ import ZODB
 
 class ZODBClientThread(threading.Thread):
 
-    sleep_time = 15
+    sleep_time = 3
 
     def __init__(self, db, test):
         threading.Thread.__init__(self)
diff --git a/tox.ini b/tox.ini
index e64b8b002cf7384982d70f6eca8d4973db0f31e3..c307c31b950770504f9d8958031aac12fc417b09 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27,py35,py36,py37,py38,pypy,pypy3
+envlist = py27,py35,py36,py37,py38,pypy,pypy3,py38-pure
 
 [testenv]
 # ZODB.tests.testdocumentation needs to find
@@ -22,3 +22,9 @@ commands =
 deps =
     {[testenv]deps}
     coverage
+
+[testenv:py38-pure]
+basepython =
+    python3.8
+setenv =
+    PURE_PYTHON = 1