Diagnosing Memory Leaks in Long-Running AnyLogic Simulations

The Problem: Performance Degradation Over Simulation Time

       The issue appeared only during long-running AnyLogic simulations. While short runs performed normally, the simulation gradually slowed down as the simulation horizon increased. Each subsequent month took longer to calculate than the previous one, indicating that some resource was accumulating over time. The model continued to produce correct results, but its computational performance progressively degraded. This behavior pointed to a potential memory management issue rather than a problem with the simulation logic itself.
       The Anylogic simulation model itself is relatively large, representing an entire refinery from feedstock sources to final product dispatch at a detailed, third-level representation (Enterprise modes). It contains more than one hundred modeled Petroleum Refining Library objects, including sources, process units, tank farms, mixing nodes, and loading racks.

Identifying the Root Cause with VisualVM

       To identify the source of the performance degradation, we used VisualVM to monitor Java heap usage and analyze object growth during the simulation. Heap snapshots were captured at different stages of the simulation and compared to determine which object types continued to increase in number. This quickly revealed abnormal growth in the number of instances of a specific Java class associated with generated railcars (class Wagon). The analysis showed that the number of live Wagon objects increased continuously as the simulation progressed, providing a clear indication of an object retention problem.

Root Cause: Unnecessary Object Retention

       The issue occurred in the Loading Rack component of the AnyLogic-based Petroleum Refining Library (PRL), which models the loading of finished petroleum products into railcars at refinery tank farms. This allows the model to account for constraints on finished-product dispatch and pumping capacity, including limitations on how quickly products can be transferred from storage to rail transport.
       Loading Rack uses a database-defined train schedule. When the original schedule is exhausted, the scheduling algorithm automatically extends it by repeating the existing arrival pattern. This allows the model to simulate long operational periods without requiring an excessively large input schedule. The problem became visible only when this schedule extension was used for long-running simulations, where new railcar objects continued to be generated over time. The investigation revealed that every newly generated railcar was being registered in an internal object registry. These references remained even after the railcars were no longer needed by the simulation. As the schedule was extended, the registry continued to grow, keeping hundreds of thousands of obsolete objects reachable and preventing them from being garbage-collected. The resulting memory growth gradually increased the computational overhead of the simulation, causing each subsequent month to take longer to calculate.

The Architectural Fix

       The fix was to change the object registration logic. Instead of registering every PrlObject by default, registration is now controlled by an explicit condition:
if (registerInPrlObjectsHolder()) PrlObjectsHolder.add(this);
       The new registerInPrlObjectsHolder() method returns false by default. Components that require global registration can explicitly override it and return true.For Wagon, the default behavior remains false. Therefore, railcars are not retained by the global registry and can be garbage-collected once they leave the Loading Rack and are removed from the simulation. This small architectural change eliminated unnecessary object retention while preserving global registration where it is actually required.

Results

       After the fix, memory consumption remained stable throughout the long-running simulation, and the progressive slowdown disappeared. The simulation time per month also stopped increasing and remained stable at approximately 6.5 minutes. The Loading Rack continues to support automatic train schedule extension without changes to its operational behavior. This makes the component more suitable for long-running refinery digital twin simulations where large numbers of railcar objects may be created over extended simulation horizons. The fix will be included in the upcoming Petroleum Refining Library (PRL) 2.2.3 release.

Lessons for AnyLogic Model Developers

       This case highlights an important aspect of AnyLogic simulation performance: creating objects is not necessarily a problem, but retaining objects that are no longer needed can become critical in long-running simulations. For models that dynamically generate large numbers of agents or other Java objects, it is important to monitor object lifecycles and periodically analyze heap usage. Tools such as VisualVM and heap snapshots can help identify unexpected object growth and retained references. Performance optimization is not always about making algorithms faster. In long-running simulations, proper object lifecycle and memory management can be equally important.

FAQ

1. Why did the AnyLogic simulation become slower over time?
The simulation gradually accumulated objects that were no longer required. As the number of retained objects increased, memory usage grew and each subsequent simulation period took longer to calculate.

2. Why was the problem visible only in long-running simulations?
Short simulations did not generate enough objects to expose the memory retention problem. The issue became significant only after the model had been running for an extended period with automatic schedule extension.

3. How was the memory leak identified?
VisualVM was used to monitor Java heap usage and compare heap snapshots captured at different stages of the simulation. The analysis revealed continuous growth in the number of instances of a specific Java class.

4. What caused the memory leak?
Newly generated Wagon objects were retained in an internal object registry even after they were no longer needed. These references prevented Java Garbage Collection from reclaiming the associated memory.

5. How was the problem fixed?
The object registration mechanism was changed so that PrlObject instances are no longer registered in the global PrlObjectsHolder by default. Registration is now enabled only for components that explicitly require it. As a result, Wagon objects are no longer retained by the global registry and can be garbage-collected after they leave the Loading Rack.

6. Did the fix change the Loading Rack behavior?
No. The automatic train schedule extension and the operational behavior of the Loading Rack remain unchanged. The fix only improved internal memory management.

7. Can the same problem occur in other AnyLogic models?
Yes. AnyLogic models that dynamically create large numbers of agents or Java objects can experience similar issues if obsolete objects remain referenced by collections, registries, or other long-lived objects. Monitoring object growth is particularly important for long-running simulations.