Profiling garbage collection in a Java app with NetBeans
Garbage collection (GC) is automatic, but it still has a visible effect on a Java application. When the heap fills quickly, the JVM pauses threads, consumes CPU, or expands memory usage until a container, virtual machine, or operating system begins to apply pressure. NetBeans IDE provides practical profiling tools for observing these behaviours while an application runs.
The useful question is rarely “How do I turn GC off?” A better question is which objects are being allocated, how long they remain reachable, and whether collection pauses match the application’s workload. A short profiling session can reveal whether the problem comes from excessive temporary objects, a growing cache, oversized responses, or an unsuitable heap limit.
This approach suits local development as well as services deployed in Australian cloud regions. A team serving customers in Sydney, Melbourne, or Brisbane may see different response times from a laptop, a staging server, and production infrastructure in the Sydney region. Measurements from the same workload are therefore more valuable than assumptions based on a developer machine.
NetBeans can combine CPU observations, memory profiling, heap information, and garbage collector activity in one workflow. The results become especially useful when they are recorded alongside request rate, response time, and application logs, rather than treated as an isolated graph.
Prepare the application and IDE
Start with a repeatable run configuration. Use the same Java version, JVM arguments, database seed data, and representative requests for each test. A small web service might need a script that sends several hundred requests, while a batch program may require a realistic input file. Keep the test long enough to include warm-up and steady-state periods.
Open the project in NetBeans and make sure the application starts correctly without the profiler. For a multi-project build, the Maven multi-module guide can help establish a clean structure before profiling. Separate modules make it easier to identify whether allocations originate in an API layer, persistence component, or shared library.
Use a development or staging environment rather than a busy production process. Profiling adds overhead, and Australian teams working across AEST and other time zones should record the test start time, deployed commit, Java release, and machine size. These details prevent a later comparison from mixing a Sydney morning test with a different build or traffic pattern.
Capture a useful baseline
Launch the application normally and allow it to warm up. Framework startup, class loading, connection pools, and JIT compilation can create allocation patterns that disappear after several minutes. Begin recording once the service reaches its ordinary operating state, then perform a fixed workload for a defined period.
In NetBeans, open the profiling controls and select the memory or Java application profiling options available for the installed version. Attach to a running JVM or start the project through the profiler, then inspect VM telemetry and memory-related views. Capture heap usage before and after the workload, collection frequency, pause behaviour, and the amount of memory left after a full collection.
A baseline should include application metrics as well. Note throughput, median latency, high-percentile latency, CPU use, and errors. A garbage collection event that lasts 50 milliseconds may be harmless for a background import but important for an interactive booking service. The business context determines which GC signal matters.
Read heap and garbage collector telemetry
The heap graph shows how occupied memory changes over time. A saw-tooth pattern is usually normal: allocations raise used heap, GC reduces it, and the cycle repeats. The important feature is the post-collection level. If that level gradually rises, live objects are accumulating or the workload is retaining more data than expected.
Frequent small collections can indicate a high allocation rate, while occasional long pauses may point to a crowded old generation or a large object graph. The exact terminology depends on the collector and Java version, so record the selected collector rather than relying on generic labels. A rising committed heap is not automatically a leak; the JVM may reserve memory in anticipation of future work.
Look for a widening gap between used and committed memory, repeated full collections, and long periods where the application is unable to make progress. In a container, compare heap behaviour with the container memory limit. A service sized for a modest Brisbane startup environment may behave differently after a deployment to a larger host with a different limit and traffic profile.
Find allocation hotspots
Memory profiling can show classes with high allocation counts or large total allocated volume. A class that allocates millions of short-lived objects may create GC work without causing a leak. Common examples include temporary strings, parsed JSON nodes, boxed primitive values, collections created inside loops, and repeated date or formatting objects.
Switch between allocation views and retained-object views where available. Allocation volume answers “What is being created?” Retained size answers “What remains reachable?” A cache, static collection, listener registration, thread-local value, or unclosed resource can retain objects long after the request that created them has ended.
Use stack traces to follow an allocation back to application code. If a report points to a framework method, inspect the call path leading into it rather than changing framework settings immediately. For a Micronaut service, this Micronaut NetBeans walkthrough provides useful project context before you compare request handling and memory activity.
Test fixes without fooling yourself
Change one factor at a time. Reusing a formatter, reducing unnecessary object conversion, bounding a cache, or streaming a large result can alter allocation behaviour. Run the same warm-up and workload after each change. Keep the profiler settings consistent, because sampling and instrumentation can affect both CPU time and allocation rates.
A heap dump can help when memory remains high after collection. Inspect paths from garbage collection roots and look for unexpected ownership: maps keyed by request data, event subscribers, executor tasks, or static registries are frequent causes. A dump is a snapshot, so take more than one when possible and compare which object groups continue to grow.
GC tuning flags should come after code and workload investigation. Increasing the heap can reduce collection frequency while allowing a leak to grow for longer. Reducing allocation is often more durable than simply giving the JVM more memory. During a change window near the Australian end of financial year, schedule heavy experiments away from business-critical processing and label every result clearly.
Turn findings into reliable tuning decisions
A useful profiling report links a symptom to evidence: “p95 latency rises during old-generation collections,” or “the import allocates 2 GB of temporary objects in ten minutes.” Include the Java version, collector, heap limits, workload rate, profiler mode, and machine or container size. This makes the result reproducible for colleagues and operations teams.
Automated formatting cannot fix memory behaviour, but consistent source code makes profiling changes easier to review. Teams can use a Checkstyle automation guide to keep diagnostic branches and allocation-related refactors consistent across the project.
Finish by validating the change under a load pattern close to production. Check that throughput improves without increasing errors, CPU saturation, or retained heap. For services used across Perth, Adelaide, and the east-coast capitals, include realistic request sizes and network conditions where practical. A successful result is stable behaviour over time, not simply a graph that looks quieter during one short run.