Answer
The OCaml runtime provides no guarantee regarding the relative order of finalizers for objects collected during the same GC safepoint. The execution sequence is implementation-defined and can vary based on the OCaml version, compilation mode (native vs. bytecode), and runtime configuration.
Confirmed Runtime Behavior
- No Specified Order: The OCaml language manual specifies that finalizers run when a value is reclaimed, but it does not define the sequence of execution for multiple reclaimed values.
- Execution Limits: Finalizers are executed at most once. They must not allocate OCaml heap memory that could trigger a re-entrant GC, as this can lead to undefined behavior.
- Incremental GC Impact: Since OCaml 4.02, the incremental major collector can postpone finalizer execution to subsequent major cycles, meaning finalizers from different generations may interleave.
- Shutdown Behavior: Finalizers are not guaranteed to run at program termination unless explicitly triggered via
Gc.full_major() or handled via at_exit.
Likely Explanation for Observed Patterns
In many practical scenarios, finalizers for objects reclaimed during a single minor GC appear to execute in allocation order. This is likely because the young-generation scan typically proceeds in the order of allocation. However, this is an implementation artifact, not a guarantee. This pattern can be disrupted by:
- Promotion of objects to the old generation before they become unreachable.
- The intervention of the incremental major GC.
- Changes in heap layout resulting from different
OCAMLRUNPARAM settings.
Influence of Compilation and Configuration
Native Code vs. Bytecode
While both runtimes utilize the same general GC algorithm, differences in stack layout and code generation can alter the exact moment objects become unreachable. This may change the set of objects collected in a specific cycle, thereby altering the observed sequence of finalizers.
OCAMLRUNPARAM Effects
Adjusting OCAMLRUNPARAM (e.g., changing the minor heap size with s) modifies the frequency and timing of collections. While this can change which objects are grouped together for reclamation, it does not introduce any formal ordering guarantees.
Verification Method
To observe this behavior in your specific environment, you can use a scoped test program that records the order of finalizer execution:
let () =
let order = ref [] in
let create_obj id =
let obj = object end in
Gc.finalise (fun () -> order := id :: !order) obj;
obj
in
let _ = create_obj 1 in
let _ = create_obj 2 in
let _ = create_obj 3 in
Gc.full_major ();
List.iter (Printf.printf "%d\n") (List.rev !order)
Run this test across different versions (e.g., 4.14, 5.0) and compilation modes (ocamlc vs ocamlopt) to verify the lack of consistent ordering.
Recommendation
Avoid using finalizers for any logic that requires a specific destruction order (e.g., closing a file before closing the device handle). Instead, use explicit cleanup functions or reference counting.