diff --git a/Include/pythonrun.h b/Include/pythonrun.h index e8a582d50a5..66766dd0bb8 100644 --- a/Include/pythonrun.h +++ b/Include/pythonrun.h @@ -217,6 +217,7 @@ PyAPI_FUNC(void) PyBytes_Fini(void); PyAPI_FUNC(void) PyByteArray_Fini(void); PyAPI_FUNC(void) PyFloat_Fini(void); PyAPI_FUNC(void) PyOS_FiniInterrupts(void); +PyAPI_FUNC(void) _PyGC_DumpShutdownStats(void); PyAPI_FUNC(void) _PyGC_Fini(void); PyAPI_FUNC(void) PySlice_Fini(void); PyAPI_FUNC(void) _PyType_Fini(void); diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 85dbc97bb2a..6b52e5a66e0 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1,6 +1,8 @@ import unittest from test.support import (verbose, refcount_test, run_unittest, strip_python_stderr) +from test.script_helper import assert_python_ok, make_script, temp_dir + import sys import time import gc @@ -610,6 +612,40 @@ class GCTests(unittest.TestCase): stderr = run_command(code % "gc.DEBUG_SAVEALL") self.assertNotIn(b"uncollectable objects at shutdown", stderr) + def test_gc_main_module_at_shutdown(self): + # Create a reference cycle through the __main__ module and check + # it gets collected at interpreter shutdown. + code = """if 1: + import weakref + class C: + def __del__(self): + print('__del__ called') + l = [C()] + l.append(l) + """ + rc, out, err = assert_python_ok('-c', code) + self.assertEqual(out.strip(), b'__del__ called') + + def test_gc_ordinary_module_at_shutdown(self): + # Same as above, but with a non-__main__ module. + with temp_dir() as script_dir: + module = """if 1: + import weakref + class C: + def __del__(self): + print('__del__ called') + l = [C()] + l.append(l) + """ + code = """if 1: + import sys + sys.path.insert(0, %r) + import gctest + """ % (script_dir,) + make_script(script_dir, 'gctest', module) + rc, out, err = assert_python_ok('-c', code) + self.assertEqual(out.strip(), b'__del__ called') + def test_get_stats(self): stats = gc.get_stats() self.assertEqual(len(stats), 3) diff --git a/Misc/NEWS b/Misc/NEWS index 76a7e57dc5c..3a9dd7a93f5 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,9 @@ What's New in Python 3.4.0 Alpha 1? Core and Builtins ----------------- +- Issue #1545463: Global variables caught in reference cycles are now + garbage-collected at shutdown. + - Issue #17094: Clear stale thread states after fork(). Note that this is a potentially disruptive change since it may release some system resources which would otherwise remain perpetually alive (e.g. database diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 9ac594fc7c8..c9c1252d345 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -1544,8 +1544,9 @@ PyGC_Collect(void) return n; } + void -_PyGC_Fini(void) +_PyGC_DumpShutdownStats(void) { if (!(debug & DEBUG_SAVEALL) && garbage != NULL && PyList_GET_SIZE(garbage) > 0) { @@ -1574,6 +1575,11 @@ _PyGC_Fini(void) Py_XDECREF(bytes); } } +} + +void +_PyGC_Fini(void) +{ Py_CLEAR(callbacks); } diff --git a/Python/import.c b/Python/import.c index b77eda106d8..cd4fb788a10 100644 --- a/Python/import.c +++ b/Python/import.c @@ -403,6 +403,14 @@ PyImport_Cleanup(void) } } + /* Collect garbage remaining after deleting the modules. Mostly + reference cycles created by classes. */ + PyGC_Collect(); + + /* Dump GC stats before it's too late, since it uses the warnings + machinery. */ + _PyGC_DumpShutdownStats(); + /* Next, delete sys and builtins (in that order) */ value = PyDict_GetItemString(modules, "sys"); if (value != NULL && PyModule_Check(value)) { diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 96b0988ec70..02a43299504 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -544,10 +544,6 @@ Py_Finalize(void) while (PyGC_Collect() > 0) /* nothing */; #endif - /* We run this while most interpreter state is still alive, so that - debug information can be printed out */ - _PyGC_Fini(); - /* Destroy all modules */ PyImport_Cleanup(); @@ -628,6 +624,7 @@ Py_Finalize(void) PyFloat_Fini(); PyDict_Fini(); PySlice_Fini(); + _PyGC_Fini(); /* Cleanup Unicode implementation */ _PyUnicode_Fini();