Typically mark-and-sweep collectors like this does compacting after scanning of heaps. So there is not false positives of keeping live blocks with dead values around. After all live objects are copied to new place, old block of heap is released.
That's pretty much what any modern GC-collected language does. .NET does this, Java does this, etc.
.NET, for example, has 3 generations - 0, 1, 2. When small objects are allocated they go into gen0 collections only. When they survive long enough they get promoted to higher gen collection. Large objects goes into separate heap that are not compacted.
Scanning for live objects mark-and-sweep style is pretty standard method. Nothing special about ocaml. Modern .NET & Java versions have pretty advanced mark-and-sweep style compacting collector. They can use multiple threads to be faster. They can do incremental scans to not take too much time. They do not suspend all threads, but does part of collection while some threads are still running (old style mark-and-sweep collectors always suspended all managed threads when doing GC).
Here are bunch of articles on .NET GC:
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/