Giving memory back
Line 1 makes a list. Python takes memory for it on the heap, the area that holds objects a program makes while it runs. The name a refers to it. Memory is limited, so it must be given back later.
In C you give memory back by hand: malloc() asks for it, free() returns it. Forget free() and the memory leaks. Use it after free() and you have a use-after-free bug. Python does this job for you.
Reference counting
CPython, the standard Python, keeps a reference count inside every object: how many references point at it. b = a gives the same list a second name, so its count goes from 1 to 2.
del a removes the name a, not the list. The count drops to 1. The list stays, because b still refers to it.
b = [3] makes a new list and points b at it. Nothing refers to the old list any more. Its count hits 0, and CPython frees its memory at once, on this very line.
Cycles
Line 5 makes two lists. y's list holds a reference to x's list, so x's list has two references (the name x and the slot inside y's list). y's list has one.
x.append(y) puts a reference to y's list inside x's list. Now each list refers to the other: a reference cycle. Both counts are 2.
del x, y removes both names. The program can never reach these lists again, but each still refers to the other, so both counts stay at 1. Counting alone will never free them.
gc.collect() runs CPython's cycle detector, from the gc module. It finds groups of objects that refer only to each other, with no way in from the program, and frees them. It also runs by itself from time to time.
Mark and sweep
Java, Go and JavaScript don't count. Rewind to just after line 7: a tracing collector starts at the roots, the variables still in use, and follows every reference, marking each object it reaches. Only b's list gets marked.
Then the collector sweeps the whole heap. Every object without a mark is garbage and is freed, so both lists in the cycle go together. Cycles cause no trouble: only reachability counts.
A simple collector stops the program while it marks and sweeps: a pause. Go and modern Java collectors do most of the work alongside the program to keep pauses short. Reference counting spreads the work out instead.
Try it in the Python prompt
;; step 1/12 · line 1: a = [1, 2] >>> a = [1, 2] >>> print(a) [1, 2] >>> import sys >>> sys.getrefcount(a) - 1 # one reference: the name a 1
sys.getrefcount() reports one more than you might expect, because passing the object to the function adds a temporary reference. The counts in the figure subtract that one. Reference counting is how CPython, the standard Python, works. Other Pythons differ: PyPy, for example, uses a tracing collector, so an object is not always freed the moment its last reference goes.