The safety net has a time limit, set by three settings (12). git gc, which Git 1,932 also runs on its own now and then (git gc --auto, triggered by about 6,700 loose objects), first expires old reflog entries: after gc.reflogExpire, 90 days by default, for entries still reachable from the branch tip, and after gc.reflogExpireUnreachable, 30 days, for entries a reset or rebase left behind. Objects that nothing references any more are then deleted once they are older than gc.pruneExpire, two weeks. A copy of BookNest's .git shows what forcing all three to "now" does:
cp -a .git ../gc-lab.git && cd ../gc-lab.git
git fsck --no-progress --unreachable --no-reflogs | wc -l
git reflog expire --expire-unreachable=now --all
git gc -q --prune=now
git fsck --no-progress --unreachable --no-reflogs | wc -l
cd ../booknest && rm -rf ../gc-lab.git72 0
--unreachable --no-reflogs counts everything that only a reflog, or nothing, still holds, such as the commits rewritten in Rebase and Conflicts and Working with Remotes, the rebase copies of Undoing a Bad Rebase and the dangling tags. After the expiry and a prune with no grace period, all 72 objects are gone for good, and no command can bring them back. Never run that pair in a repository you might need to recover from. The defaults give you a month for reset or rebased-away commits, three months for the rest; git config gc.reflogExpireUnreachable 90.days buys more time on a project where that matters.
