Skip to content

Conversation

@daniel-raffler
Copy link
Contributor

Hello,

this PR aims to fix memory leaks on CVC5 and Bitwuzla

  • CVC5 We now use Context.deletePointers to make sure all solver memory is freed. Context is a utility class in the CVC5 JNI bindings that tracks all solver objects. The deletePointers method can then be called at the end of the program to free native memory. It is not tied to a TermManager and will delete Objects from all remaining CVC5 SolverContexts, which is why it can only be used when closing the last solver instance.
    We used to only close Solver and TermManager when a CVC5 SolverContext is closed. While this approach is more fine-grained, it also leads to memory leaks as Terms and Sorts are never freed, even after the SolverContext is closed. Our new solution is to check in SolverContext.close() if we're closing the last CVC5 instance and then delete all Objects with deletePointers. Otherwise only the Solver object is deleted right away, and all other Objects will be collected at the end of the Program. (Thanks to @baierd for the suggestion)
  • Bitwuzla We adopt a very similar approach to CVC5. Unlike CVC5, Bitwuzla does not come with its own Java bindings, and we use SWIG to generate our own JNI headers. SWIG uses finalizers to release native memory when it is no longer needed. This allows for more fine-grained control, but is also error-prone, and we had to disable several of the finalizers to avoid random crashes in Bitwuzla. This lead to memory leaks, which have now been closed by tracking all solver objects, similar to how it is done in CVC5

Here are some benchmarks to show that the leaks have indeed been closed. The program executes the same JavaSMT trace in a loop while monitoring total memory usage of the process:

CVC5 (before)
cvc5-0

CVC5 (after)
cvc5-1

Bitwuzla (before)
bitwuzla-0

Bitwuzla (after)
bitwuzla-1

@kfriedberger
We will need to recompile the Bitwuzla bindings for this PR. Could you handle this, please?

We use an approach very similar to what CVC5 is doing in its Java bindings. SWIG uses finalizers to free native memory, however, this makes it very difficult to control the order in which objects are freed. It's also impossible to debug. However, I believe that the crashes were caused by running a finalizer of a Term/Sort object *after* the TermManager was already deleted
We're now deleting Terms when the SolverContext is closed, so they can no longer be accessed in the test
We would have to wait for the thread to make sure it's not still running when the TermManager is destroyed by the @after method. Since the test seems to be about an old bug that has since been fixed, I'm simply removing it
kfriedberger
kfriedberger previously approved these changes Jan 20, 2026
Copy link
Member

@kfriedberger kfriedberger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done. Thanks for the analysis and solution.

I will try to find some time soon for updating the solver library.

Side note: Which tool was used to generate the graphs for measurements? JProfiler or similar?

@daniel-raffler
Copy link
Contributor Author

Well done. Thanks for the analysis and solution.

Thanks!

Side note: Which tool was used to generate the graphs for measurements? JProfiler or similar?

I used procpath for the graphs and wrote a small program to repeatably run the same JavaSMT trace so that we can monitor its memory usage. Here is the source for the tool. If needed, this could also be integrated into JavaSMT for benchmarking. The other approach is to use CPA-Daemon to look for memory leaks, but then it's not entirely clear if the problem is with JavaSMT or CPAchecker. Profilers weren't too useful as they can only see the Java heap, and not what is going on in native memory when using JNI

@daniel-raffler
Copy link
Contributor Author

Our new solution is to check in SolverContext.close() if we're closing the last CVC5 instance and then delete all Objects with deletePointers. Otherwise only the Solver object is deleted right away, and all other Objects will be collected at the end of the Program

One more note: This solution works well if there is only a single SolverContext, or when only using SolverContexts sequentially (i.e closing the last SolverContext before opening a new one). It might work less well in a parallel setting when SolverContexts keep being opened and closed in parallel. While the code should be thread-safe, memory will only be freed whenever we get to a point where all SolverContexts are closed. If there are multiple busy threads this might not happen until the end of the program, and memory will still pile up in the mean time

We could fix this by managing references for each TermManager, rather than using a global queue. However, this adds additional overhead, and for CVC5 we would have to ask the developers for a fix as we don't use our own JNI bindings

@kfriedberger kfriedberger merged commit 1864779 into master Jan 24, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Memory leaks on CVC4/5 and Bitwuzla

3 participants