You do have to manually call Addref/ReleaseRef.
The compiler does not do this for you.
O1 := MyTRefCountedObject.Create;
O1.AddReference({$IFDEF WITH_REFCOUNT_DEBUG}'Just created', self {$ENDIF});
O2:=O1;
O2.AddReference();
It is just a framework.
It has some very limited support for weak refs. But I would not go to far into using them. (overloading := would not break circle refs either)
ReleaseRef must have matching params !!! That ensures your AddRef/ReleaseRef are correctly paired.
Search components/fpdebug for WITH_REFCOUNT_DEBUG, you get plenty of examples.
As you already have a codebase, it will unfortunately mean a lot of work to put it all together.
If you are just looking to find a single (or a few) accesses to no longer existing objects:
On linux: Use valgring (note, this needs "uses cmem").
This gives you where unallocated mem was read or written. Where it was previously allocated, and where it was freed.
It is worth getting a linux VM, just for having valgrind.
Otherwise:
Make sure you use heaptrc. And set the environment HEAPTRC="keepreleased"
That means memory will not get re-used (or at least not soon)
Heaptrc will (with delay) tell you if your app wrote (only wrote) to unused memory.
IIRC it will tell you where the mem got allocated.
From there it is still a lot of work, and a lot of try and error.
There are two features of the debugger I often use in cases like this.
Debugger History: Setting a breakpoint in the destructor (assuming I know the class that is involved). Record the stack (and sometimes some watches, like the address of self), and autocontinue the app.
That way I do not have to keep pressing F9. But I can later find a list of all calls in the History window.
Then when the memory gets accessed (assuming it causes a crash, right then), I can get the address of the object that was incorrectly accessed, and go through the list of destroy calls.
The other is, if I know the exact instance that gets accessed after destroy, and which field is accessed, but not when. Break in the destroy, and set a watchpoint on the field. The debugger will stop if the memory is accessed (read or write). But you must already know which instance causes the issue.