diff --git a/CHANGES.rst b/CHANGES.rst
index 542e403..81a77a8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -12,6 +12,10 @@
   when setting its ``__class__`` and ``__dict__``. This matches the
   behaviour of the C implementation. See `issue 155
   <https://github.com/zopefoundation/persistent/issues/155>`_.
+- Fix the CFFI cache implementation (used on CPython when
+  ``PURE_PYTHON=1``) to not print unraisable ``AttributeErrors`` from
+  ``_WeakValueDictionary`` during garbage collection. See `issue 150
+  <https://github.com/zopefoundation/persistent/issues/150>`_.
 
 4.6.4 (2020-03-26)
 ==================
diff --git a/persistent/picklecache.py b/persistent/picklecache.py
index 43d5067..3dde6c3 100644
--- a/persistent/picklecache.py
+++ b/persistent/picklecache.py
@@ -102,7 +102,13 @@ class _WeakValueDictionary(object):
             return self._cast(addr, self._py_object).value
 
         def cleanup_hook(self, cdata):
-            oid = self._addr_to_oid.pop(cdata.pobj_id, None)
+            # This is called during GC, possibly at interpreter shutdown
+            # when the __dict__ of this object may have already been cleared.
+            try:
+                addr_to_oid = self._addr_to_oid
+            except AttributeError:
+                return
+            oid = addr_to_oid.pop(cdata.pobj_id, None)
             self._data.pop(oid, None)
 
     def __contains__(self, oid):

diff --git a/persistent/tests/test_picklecache.py b/persistent/tests/test_picklecache.py
index 32811f1..9d6ec25 100644
--- a/persistent/tests/test_picklecache.py
+++ b/persistent/tests/test_picklecache.py
@@ -1102,6 +1102,44 @@ class PythonPickleCacheTests(PickleCacheTestMixin, unittest.TestCase):
         self.assertEqual(cache.cache_non_ghost_count, 0)
         self.assertEqual(len(cache), 0)
 
+    def test_interpreter_finalization_ffi_cleanup(self):
+        # When the interpreter is busy garbage collecting old objects
+        # and clearing their __dict__ in random orders, the CFFI cleanup
+        # ``ffi.gc()`` cleanup hooks we use on CPython don't
+        # raise errors.
+        #
+        # Prior to Python 3.8, when ``sys.unraisablehook`` was added,
+        # the only way to know if this test fails is to look for AttributeError
+        # on stderr.
+        #
+        # But wait, it gets worse. Prior to https://foss.heptapod.net/pypy/cffi/-/issues/492
+        # (CFFI > 1.14.5, unreleased at this writing), CFFI ignores
+        # ``sys.unraisablehook``, so even on 3.8 the only way to know
+        # a failure is to watch stderr.
+        #
+        # See https://github.com/zopefoundation/persistent/issues/150
+
+        import sys
+        unraised = []
+        try:
+            old_hook = sys.unraisablehook
+        except AttributeError:
+            pass
+        else: # pragma: no cover
+            sys.unraisablehook = unraised.append
+            self.addCleanup(setattr, sys, 'unraisablehook', old_hook)
+
+        cache = self._makeOne()
+        oid = self._numbered_oid(42)
+        o = cache[oid] = self._makePersist(oid=oid)
+        # Clear the dict, or at least part of it.
+        # This is coupled to ``cleanup_hook``
+        if cache.data.cleanup_hook:
+            del cache.data._addr_to_oid
+        del cache[oid]
+
+        self.assertEqual(unraised, [])
+
 
 @skipIfNoCExtension
 class CPickleCacheTests(PickleCacheTestMixin, unittest.TestCase):
@@ -1182,6 +1220,30 @@ class CPickleCacheTests(PickleCacheTestMixin, unittest.TestCase):
         self.assertEqual(len(cache), 0)
 
 
+class TestWeakValueDictionary(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from persistent.picklecache import _WeakValueDictionary
+        return _WeakValueDictionary
+
+    def _makeOne(self):
+        return self._getTargetClass()()
+
+    @unittest.skipIf(PYPY, "PyPy doesn't have the cleanup_hook")
+    def test_cleanup_hook_gc(self):
+        # A more targeted test than ``test_interpreter_finalization_ffi_cleanup``
+        # See https://github.com/zopefoundation/persistent/issues/150
+        wvd = self._makeOne()
+
+        class cdata(object):
+            o = object()
+            pobj_id = id(o)
+        wvd['key'] = cdata.o
+
+        wvd.__dict__.clear()
+        wvd.cleanup_hook(cdata)
+
+
 def test_suite():
     return unittest.defaultTestLoader.loadTestsFromName(__name__)
 

AttributeError: '_WeakValueDictionary' object has no attribute '_addr_to_oid'
Seen during test teardown of a large app using CPython in PURE_PYTHON=1 mode

```
From callback for ffi.gc <cdata 'struct CPersistentRingCFFI_struct *' owning 24 bytes>:
Traceback (most recent call last):
  File "//persistent/persistent/picklecache.py", line 105, in cleanup_hook
    oid = self._addr_to_oid.pop(cdata.pobj_id, None)
AttributeError: '_WeakValueDictionary' object has no attribute '_addr_to_oid'
```

This is ignored, and the dict is already gone, so there shouldn't be any harm, but it is annoying.
persistent/tests/test_picklecache.py::TestWeakValueDictionary::test_cleanup_hook_gc
persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___delitem___non_string_oid_raises_TypeError persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___delitem___nonesuch_raises_KeyError persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___delitem___w_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___delitem___w_normal_object persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___delitem___w_persistent_class persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___delitem___w_remaining_object persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___getitem___nonesuch_raises_KeyError persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___setitem___duplicate_oid_raises_ValueError persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___setitem___duplicate_oid_same_obj persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___setitem___ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___setitem___mismatch_key_oid persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___setitem___non_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___setitem___non_string_oid_raises_TypeError persistent/tests/test_picklecache.py::PythonPickleCacheTests::test___setitem___persistent_class persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_cache_garbage_collection_bytes_also_deactivates_object persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_cache_garbage_collection_bytes_with_cache_size_0 persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_cache_raw persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_cache_size persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_cannot_update_mru_while_already_locked persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_class_conforms_to_IPickleCache persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_debug_info_w_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_debug_info_w_normal_object persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_debug_info_w_persistent_class persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_empty persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_full_sweep persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_full_sweep_w_changed persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_full_sweep_w_sticky persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_get_nonesuch_no_default persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_get_nonesuch_w_default persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_incrgc_simple persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_incrgc_w_larger_drain_resistance persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_incrgc_w_smaller_drain_resistance persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_init_with_cacheless_jar persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_instance_conforms_to_IPickleCache persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_interpreter_finalization_ffi_cleanup persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_hit_multiple_mixed persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_hit_multiple_non_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_hit_pclass persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_hit_single_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_hit_single_non_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_miss_multiple persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_miss_single persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_invalidate_persistent_class_calls_p_invalidate persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_lruitems persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_minimize persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_minimize_turns_into_ghosts persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_mru_first persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_mru_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_mru_last persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_mru_nonesuch_raises_KeyError persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_mru_normal persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_mru_was_ghost_now_active persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_non_persistent_object persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_obj_already_has_jar persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_obj_already_has_oid persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_obj_already_in_cache persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_success_already_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_success_not_already_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_w_pclass_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_new_ghost_w_pclass_non_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_reify_hit_multiple_mixed persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_reify_hit_single_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_reify_hit_single_non_ghost persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_reify_miss_multiple persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_reify_miss_single persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_setting_already_cached persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_setting_non_persistent_item persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_setting_without_jar persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_sweep_empty persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_sweep_of_non_deactivating_object persistent/tests/test_picklecache.py::PythonPickleCacheTests::test_update_object_size_estimation_simple persistent/tests/test_picklecache.py::CPickleCacheTests::test___delitem___non_string_oid_raises_TypeError persistent/tests/test_picklecache.py::CPickleCacheTests::test___delitem___nonesuch_raises_KeyError persistent/tests/test_picklecache.py::CPickleCacheTests::test___delitem___w_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test___delitem___w_normal_object persistent/tests/test_picklecache.py::CPickleCacheTests::test___delitem___w_persistent_class persistent/tests/test_picklecache.py::CPickleCacheTests::test___delitem___w_remaining_object persistent/tests/test_picklecache.py::CPickleCacheTests::test___getitem___nonesuch_raises_KeyError persistent/tests/test_picklecache.py::CPickleCacheTests::test___setitem___duplicate_oid_raises_ValueError persistent/tests/test_picklecache.py::CPickleCacheTests::test___setitem___duplicate_oid_same_obj persistent/tests/test_picklecache.py::CPickleCacheTests::test___setitem___ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test___setitem___mismatch_key_oid persistent/tests/test_picklecache.py::CPickleCacheTests::test___setitem___non_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test___setitem___non_string_oid_raises_TypeError persistent/tests/test_picklecache.py::CPickleCacheTests::test___setitem___persistent_class persistent/tests/test_picklecache.py::CPickleCacheTests::test_cache_garbage_collection_bytes_with_cache_size_0 persistent/tests/test_picklecache.py::CPickleCacheTests::test_cache_raw persistent/tests/test_picklecache.py::CPickleCacheTests::test_cache_size persistent/tests/test_picklecache.py::CPickleCacheTests::test_class_conforms_to_IPickleCache persistent/tests/test_picklecache.py::CPickleCacheTests::test_debug_info_w_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test_debug_info_w_normal_object persistent/tests/test_picklecache.py::CPickleCacheTests::test_debug_info_w_persistent_class persistent/tests/test_picklecache.py::CPickleCacheTests::test_empty persistent/tests/test_picklecache.py::CPickleCacheTests::test_full_sweep persistent/tests/test_picklecache.py::CPickleCacheTests::test_get_nonesuch_no_default persistent/tests/test_picklecache.py::CPickleCacheTests::test_get_nonesuch_w_default persistent/tests/test_picklecache.py::CPickleCacheTests::test_incrgc_simple persistent/tests/test_picklecache.py::CPickleCacheTests::test_incrgc_w_larger_drain_resistance persistent/tests/test_picklecache.py::CPickleCacheTests::test_incrgc_w_smaller_drain_resistance persistent/tests/test_picklecache.py::CPickleCacheTests::test_inst_does_not_conform_to_IExtendedPickleCache persistent/tests/test_picklecache.py::CPickleCacheTests::test_instance_conforms_to_IPickleCache persistent/tests/test_picklecache.py::CPickleCacheTests::test_invalidate_hit_multiple_non_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test_invalidate_hit_single_non_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test_invalidate_miss_multiple persistent/tests/test_picklecache.py::CPickleCacheTests::test_invalidate_miss_single persistent/tests/test_picklecache.py::CPickleCacheTests::test_invalidate_persistent_class_calls_p_invalidate persistent/tests/test_picklecache.py::CPickleCacheTests::test_lruitems persistent/tests/test_picklecache.py::CPickleCacheTests::test_minimize persistent/tests/test_picklecache.py::CPickleCacheTests::test_minimize_turns_into_ghosts persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_non_persistent_object persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_obj_already_has_jar persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_obj_already_has_oid persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_obj_already_in_cache persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_success_already_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_success_not_already_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_w_pclass_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test_new_ghost_w_pclass_non_ghost persistent/tests/test_picklecache.py::CPickleCacheTests::test_setting_already_cached persistent/tests/test_picklecache.py::CPickleCacheTests::test_setting_non_persistent_item persistent/tests/test_picklecache.py::CPickleCacheTests::test_setting_without_jar persistent/tests/test_picklecache.py::CPickleCacheTests::test_sweep_empty persistent/tests/test_picklecache.py::test_suite