Are you looking for an alternative to VisualVM? You might already be familiar with some of the issues. If your Java application is running slowly, but the CPU graph doesn’t move much, there’s no obvious answer as to why. Is your application doing nothing, or are your threads waiting for work that they can’t access? Taking a recording will get you the clues but not the answer.
When considering an alternative to VisualVM for analyzing JFRs, it’s important to see how the tools enable you to make sense of these clues. We looked at 3 specific scenarios to compare VisualVM and yCrash JFRPlayer. These were: a CPU hotspot, increasing retained memory (with temporary allocations), and contention on a single lock.
Both tools enabled us to look into the scenarios; however, there were some differences in how we got to the code, how much interpretation was provided, and what automated conclusions we had to validate. Let’s take a look at the clues.
JFR Analysis Test Environment and Methodology
In the Windows system and in the JDK21 environment, the same recording is evaluated with both tools in each scenario. The test version of VisualVM is 2.2.1. The JFRPlayer CPU report displays version 3_5. This validation is limited to the functionality of the user interface in the test version of each tool and does not refer to future updates.
| Scenario | Shared Recording File | Recording Time | Expected Behavior |
|---|---|---|---|
| CPU hotspot | cpu-spike-02.jfr | 105 seconds | One worker repeatedly calls hotMethod(long) |
| Memory growth | memory-growth-01.jfr | 116 seconds | A list retains byte arrays while another path allocates temporary arrays |
| Thread contention | thread-contention-01.jfr | 75 seconds | Four workers compete for one monitor held during a sleep |
These scenarios are synthetic, and we know the underlying source code. We have not timed any investigations, nor have we measured any benchmarking overheads or diagnostic accuracy in real production scenarios. The path to the root cause has been described here to give an indication of what evidence was required to understand the causes of each scenario.
The attached source_code folder contains the complete Java program for playing back workloads. The following excerpts highlight relevant actions. Storing differs in the number of events, timing, and sampling rate.
VisualVM vs. yCrash JFRPlayer: Key Differences
These tools overlap in their ability to expose the JVM’s behavior, but their starting point is different. One tool provides direct access to the event and timeline, while another tool adds a written interpretation to the report.
VisualVM Features for Java Performance Analysis
VisualVM is a free desktop troubleshooting tool. The official feature guide explains runtime monitoring, thread visualization, profiling, and heap dump analysis. The JFR interface we tested used Monitor, Threads, Sampler, and Browser views to link symptoms to recorded events.
- The CPU call tree helps to identify CPU-intensive methods in the selected thread.
- Heap and GC views help explore memory behavior over time.
- The Event Browser displays individual tasks and event tracking using stored stack traces.
Install a compatible standalone version from the official download page and open the record → by uploading the file. We used 2.2.1 for our successful offline workflows, but this will not be displayed as the latest release. Find out the documentation for a specific distribution before following the instructions of the old plugin.
This workflow is suitable for engineers who want to examine the underlying evidence and choose their own filters. You also need to understand the scope of each event and correlate the results between views. As an example, we’ve used the call tree function within VisualVM to look into the hotspot of our CPU Test further down in this article.
yCrash JFRPlayer Features for JFR Analysis
yCrash JFRPlayer receives the uploaded JFR file and provides a browser report with an overview of AI.JFR’s FAQ document describes cloud and local deployment, and its analysis method describes the interpretation of AI after programmatic analysis of structured discoveries.
- A report summarizing the JVM’s performance charts and the results of the problem-centered analysis.
- Artificial intelligence briefings provide detailed comments and suggestions for next steps.
- Full REST APIs, archive reports, and compare data.
Note: Only manual downloading of reports was undertaken within this test. There was no verification of automation using the API, security, or report comparison. Due to the license type and deployment of the product, there will be some variance in features and AI tools. The browser is not free to use all the features.
This report will give you a good overview of the issues. You should make sure to look at the execution log and source code to make sure that your analysis matches what the app was doing. This is particularly important for memory test cases.
Comparing Three Java Performance Scenarios with VisualVM and yCrash JFRPlayer
We compared VisualVM and yCrash JFRPlayer using three controlled Java workloads: a CPU hotspot, memory growth, and thread contention. Since the underlying source code was known, we could examine how each tool presented the evidence, identified the relevant methods, and helped us understand the cause of each scenario.
1. Java CPU Profiling: Identifying Performance Hotspots
In the first test scenario, we set clear detection targets for these two tools.CpuSpikeDemo program waits 10 seconds after booting, performs a high-load operation for about 90 seconds, and then waits another 5 seconds to exit. During this time, the executor repeats the following formula action:
private static long hotMethod(long seed) { long value = seed; for (int i = 0; i < 1_000_000; i++) { value = value * 1664525L + 1013904223L; value ^= value >>> 13; } return value;}
This recording has 5,617 instances of jdk.ExecutionSample. This was a deliberate loop in the code to ensure that each workflow leads to the well-known compute-intensive method.
1.1 How VisualVM Identifies Java CPU Hotspots
We opened up the Sampler tool under CPU. We have expanded the cpu-hot-worker call tree. VisualVM has shown us that around 89,804ms (99.9%) of time has been spent inside hotMethod() in the worker. However, these figures are based on sampling and do not represent the total amount of CPU used across the application.
Fig: VisualVM CPU profiling identifies hotMethod() in the cpu-hot-worker call tree.
The call tree provides information about what the thread calls with the lambda and the computational function. This allows a specific decision to be made on whether to investigate the loop further.
Note, however, that this number is a ratio in the display area and time is an estimate of the sample data. It differs from the overall CPU usage of the application. Just because it is dominant in the work cycle does not mean that it is an immediate problem.
1.2 How yCrash JFRPlayer Identifies CPU Hotspots
yCrash JFRPlayer pointed out that it is a combination of CpuSpikeDemo.hotMethod(long) and cpu-hot-worker. It had been subjected to an awesome sample of 5,612 times. Its flame graph showed the corresponding call path.
Fig: yCrash JFRPlayer CPU flame graph traces the execution path to hotMethod(long).
The auto-summary will show you the techniques and threads that the original hints were picked up on. The diagram shows the full call path used (such as X-rays). Don’t get confused between the number of samples per technique and the total number of events in your whole record.
Practical aspects: Both approaches have succeeded in identifying critical points. The desktop call table is exploratory, and the report is hypothetical, but in the end the engineer’s assessment says: “Is this calculation really necessary? How can we improve while maintaining the desired functionality? ”
For more detailed analysis examples, see “Java CPU peak issues resolved with JFR”.
2. Java Memory Analysis: Tracking Memory Growth
Memory-intensive paths are not always the real culprits. To highlight this difference, MemoryGrowthDemo performs a two-step operation in each of the 90 steps: retaining a 1 MiB byte sequence as a “hostage”, it creates a temporary 16 MiB sequence as a “passerby”.
private static void growCollection() { for (int i = 0; i < 16; i++) { byte[] block = new byte[64 * 1024]; block[0] = (byte) i; retained.add(block); }}private static void createTemporaryObjects() { for (int i = 0; i < 256; i++) { byte[] temporary = new byte[64 * 1024]; temporary[0] = (byte) i; checksum += System.identityHashCode(temporary); }}
The program has short pauses for each stage of processing. This means that memory is kept allocated for a period of 15 seconds. By design, it will reserve 90MiB of memory (excluding Object Headers and garbage collection overheads). This is a designed feature and is not caused by a memory leak or GC issue within the production environment.
2.1 Using VisualVM to Investigate Memory Allocation
The post-GC heap usage has increased. Using Browser -> Object Allocation Sample, we checked the byte[] events for memory-growth-worker. By following the stack trace, we found that it leads us to the growCollection() method (line 48) and createTemporaryObjects() method (line 57) in the source code.
Fig: VisualVM allocation sampling traces memory allocation to the growCollection() method.
These traces clearly distinguish two different memory reserve routes. After reviewing the source code in detail, we found that one keeps the reference in a static list, while the other creates temporary arrays without retaining references to them after the method returns.
A total of 3,047 samples were recorded in the browser. The Sample Weight column indicates the weight of the sample. This does not represent how much each individual item weighs nor how much real-time data was taken. Whilst you can see where each item was created using the call stack, there is no indication as to why it is still being referenced.
2.2 How yCrash JFRPlayer Reports Java Memory Growth
In the “Post-GC Heap Usage” chart in the JFRPlayer report by yCrash, the baseline had risen to the right, starting from a few megabytes and progressing towards about 100 megabytes.
Fig: yCrash JFRPlayer report shows increasing post-GC heap usage during the memory growth test.
The AI summary highlighted the iterative allocation of byte arrays and memory variation. This phenomenon corresponds to the path of a temporary task. However, it is not clear in the current summary that the constant increase in the storage list is the real reason for the increase in the use of the heap.
Fig: yCrash JFRPlayer memory overview highlights allocation patterns that require further investigation.
The report specifically refers to a number of NoSuchMethodError(s) that may indicate version inconsistencies in dependent libraries. We carefully checked the call stack when an error occurred: three errors focused on the start-up phase that occurred just milliseconds before and after, all within the ProcessHandle initialization and method handle search paths. In particular, the workload ended successfully. We cannot conclude from these findings that subsequent memory additions are due to library inconsistencies, so we do not make dependency corrections based solely on current evidence.
The Heap Alternate view showed a mix of byte arrays that were larger than the maximum defined size of 256 MiB. As such, we are not taking this as an actual snapshot of objects in memory. An important thing to note here is that if you do not understand what a metric means, then any alternate data extracted from JFR should not be considered an actual heap dump.
Practically, the graph shows increasing post-GC heap usage. The summary demonstrates the allocation functionality. However, a more descriptive explanation was created by linking together the trends, stack of tasks, and source references back to the source. An unknown application may need a heap dump to produce a chain of reference retention. This is covered in the JFR FAQ.
3. Java Thread Contention Analysis
4 worker threads are contending for access to the same object monitor. Each time a thread successfully gains access to the monitor, it sleeps for 100ms.
private static void slowOperation() throws InterruptedException { synchronized (LOCK) { Thread.sleep(100); completedOperations++; }}
The operation of this program includes a base phase of 10 seconds, a conflict phase of about 60 seconds, and a final holding phase of 5 seconds.I did it and performed 553 operations. Analysis of 75 seconds of recorded data confirmed 552 jdk.JavaMonitorEnter events. It should be noted, however, that this number of events is not directly related to the number of message threads, and not all monitor search actions are recorded as conflicting events.
3.1 Investigating Thread Contention with VisualVM
When we look at the Threads timeline, we can see that each employee thread periodically repeats the acquisition status and standby status of the monitor. We then opened the → Java Monitor block of the browser and selected a specific event that shows the stack tracking panel.
Fig: VisualVM thread contention analysis links a blocked lock-worker-3 thread to slowOperation() and identifies the previous monitor owner.
In the selected case, we found that the lock-worker-3 thread has waited about 100.5 milliseconds, and the previous monitor-holding thread was lock-worker-2. While tracking the stack trace of this thread, we reached the ThreadContentionDemo.slowOperation() method on row 44 of the imported source code. In particular, all confirmed transactions referred to the same monitor address.
This clearly shows that there is some contention. If you look at the source code, you’ll see why this is the case. The problem is that when the thread sleeps inside the synchronized block, no other worker threads can gain access to the monitor. The stack trace that is recorded is the information from the thread at the point where it tried to obtain the lock. It will not record all previous actions by the owner of the monitor.
3.2 Identifying Thread Contention with yCrash JFRPlayer
The AI overview report from yCrash JFRPlayer detected an object monitor discrepancy and an invitation to Thread.sleep() in the slowOperation() method. Suggestions for improvement included reducing the time required for synchronization blocks and checking the transition to sleep mode.
Fig: yCrash JFRPlayer identifies slowOperation() as a performance bottleneck and recommends reducing synchronization time.
The results are in line with the originally planned loading. This was because the analysis report explicitly described the methodology and mechanism behind each method. This meant that there was no need to choose an individual monitoring event.
The same analysis report points out that the ThreadSleepEvent.commit() framework consumes 50% of the CPU. However, we do not consider this to be a general application CPU utilization rate. Sample result percentages are attributable to different denominators. The listed chain status statistics are also not based on the four worker threads we use. The real valuable diagnostic result is the performance bottleneck mechanism validated in the source code, not all the numbers listed in the report.
Practical conclusion: Both workflows have detected slow operation. The first shows the event and stack trace in its idle state; the second is an abstract explanation of the issue and potential changes. This is simply a performance issue; there is no deadlock, and the application has finished. If we can guarantee the correct result, we may want to consider removing the long process from the sync block.
How Automated JFR Analysis Helps Java Troubleshooting
VisualVM is a free desktop tool and the first choice for technicians who already use it to monitor JVM behavior. Performance studies using JFR spend time selecting the appropriate view, expanding call trees and flame graphs, and tying this evidence to a trusted description.
Our performance bottleneck test shows how an auto-generated overview can reduce initial navigation. In VisualVM, we validated the Threads timeline, opened a blocked event on the Java screen, selected Even, and read the stack trace. The automated overview identified slowOperation() and tied monitor conflicts to sleep calls. Overview: A high-level summary of the scenario was presented first, then one specific monitor event was chosen.
In the CPU test, we also had some differences between the starting points. By expanding the workers call tree manually and using the overview to determine the busy method and thread, we can save an engineer who is not familiar with the recording. Finding the interesting starting point can save them time navigating through the views. An experienced engineer will quickly be able to find the hotspot in the call tree or flame graph.
Now it can be said that it can lead to a reduction in time at the initial screening.It is not known whether the overall diagnostic time will be reliably shortened. As we did not make measurements step by step, we cannot give precise figures such as how many minutes we were able to shorten or how many percent faster we were. However, it is impossible to determine the actual effect without carefully comparing, including downloading files, structuring, summarizing, and reviewing the contents of the summaries.
When I look at the results of the memory test, I feel that even if there are fewer navigation steps, it does not necessarily mean that you will get the correct diagnosis earlier. Although it is a summary that focuses on memory allocation problems, or a summary that shows dependency issues that have not yet been identified, it needs to be studied carefully. It ultimately matters whether the summary brings us closer to the source of the problem. Do not forget that the process always includes the indexing and validation of the source code.
How to Reproduce the JFR Benchmark Tests
The attached Java file contains the logic used in the test. Try running the following commands consecutively from the directory where the file is located in the Windows Command Prompt JDK21 set to the path; (^ means the command will continue on the next line). Save the original JFR recording for comparison and use the new filename to restart.
javac CpuSpikeDemo.java MemoryGrowthDemo.java ThreadContentionDemo.javajava ^ -XX:StartFlightRecording=filename=cpu-repeat.jfr,settings=profile,dumponexit=true ^ CpuSpikeDemojava -Xms128m -Xmx256m -XX:+UseG1GC ^ -XX:StartFlightRecording=filename=memory-repeat.jfr,settings=profile,dumponexit=true ^ MemoryGrowthDemojava ^ -XX:StartFlightRecording=filename=contention-repeat.jfr,settings=profile,dumponexit=true ^ ThreadContentionDemojfr summary contention-repeat.jfr
Run each program individually and open the corresponding recording file after finishing. Download the same completed file to both analysis tools. Save the tool version, recording settings, selected events or message threads, as well as screenshots to your notes. The availability of JFR events depends on the recording settings. For more information, see JDK 21 Java Command Reference Recording Options.
VisualVM vs. yCrash JFRPlayer: Feature Comparison
The table below has been created to provide clarity between what can be seen and what is recorded. It provides information on how to set up, the cost, automation, steps taken to investigate and when it would be suitable.
| Standards | VisualVM | JFRPlayer |
|---|---|---|
| Installation | Local desktop installation, compatible runtime environment | Browser access to cloud or predefined private servers |
| Price | Free desktop tools | You need to check JFR and AI permissions, pricing, and trial period |
| CPU Query | Extended work thread call tree showed hotMethod() | Method name mentioned in summary and flame graph showed its call path |
| Memory query | Targeting event revealed two creation paths | Post-GC charts showed growth; the summary highlighted variations but did not clearly identify the retention list |
| Thread contention analysis | The follow-up event indicated the duration, previous owner, and stack in the overview | The mentioned slowOperation() and suggested shortening the critical sections |
| Automation | Transactions investigated in the tested workflow | An overview was created, and APIs and report repositories were documented but not tested |
| Solution path | The engineer selects the event and attaches the code to the view | The summary provides clues and the engineer compiles the evidence |
| Best case | When you view events directly and use existing desktop JVM workflows | When you focus on guided report evaluations and browser-based assignments |
Which JFR Analysis Tool Should You Choose?
VisualVM is a good choice if you want to move freely between events, chains, and call stacks and if you already have the skills to parse them. This free desktop workflow is useful even if you also use the automated reporting tool.
yCrash JFRPlayer is more appropriate if you need a text guide or browser version of the report for your research or assignment. However, you will need to familiarize yourself with the terms and license of the deployment before planning access to your Incident Response Team. The reports shared can simplify the discussion, but testing does not measure the effectiveness of collaboration in this regard.
If you’re looking for a more holistic approach, check out the Efficient JFR Analysis with AI webinar. The salesperson’s explanatory video provides the functional background, and the control tests here are proof of our findings.
Conclusion: Choosing a VisualVM Alternative for JFR Analysis
This article compares two different JFR analysis workflows using known scenarios. Both tools will allow you to find CPU hotspots and methods involved in thread contention easily. However, you need to be particularly careful when analysing memory growth and make sure you distinguish between the two stages of allocating arrays and keeping references to them.
A good VisualVM option should simplify the technical decision-making process. The guided analysis shows the method of the problem, and direct transaction investigation can identify the details. In both cases, you need to understand the meaning of the metric, check the source code, and save again to confirm the change.
Optimize existing desktop workflows by continuing to solve problems with traditional tools and introducing new tools when automated analysis gives a clear direction. But don’t forget to check the impact of automated suggestions before changing the production code.

