CodeGym /Courses /JAVA 25 SELF /Profiling and code optimization: tools, approaches

Profiling and code optimization: tools, approaches

JAVA 25 SELF
Level 63 , Lesson 4
Available

1. Introduction to profiling

Profiling is like a medical examination for your program: we don’t just look at the “temperature” (monitoring), we look for where the application “hurts,” what runs slowly, and where too much memory or resources are being consumed.

Profiling is the process of collecting and analysing information about a program’s execution to identify bottlenecks and inefficient code. Unlike monitoring, which usually tracks general metrics (CPU load, memory, number of threads), profiling lets you look inside: which methods are called most often, how long they take, how many objects are created, and where exactly a memory leak occurs.

When do you really need profiling?

  • The application “lags,” but it’s unclear why.
  • Memory consumption suddenly grew.
  • After a code update, something started running longer.
  • You need to understand why the server lacks resources.

By the way, almost every developer has optimised the wrong piece of code at least once. Why? Because it’s nearly impossible to identify a bottleneck by eye — that’s what a profiler is for.

Key profiling metrics

  • Method execution time (CPU profiling): Which methods take the most time? Where does the program “spend” CPU?
  • Memory usage (memory profiling): Which objects are created most often? Where do they stay in memory longer than needed?
  • Number of objects: Are we creating too many similar objects?
  • Threads: Are there too many threads? Are there locks (deadlock, contention)?
  • Method calls: What is the stack depth? Is there unbounded recursion?

2. Profiling tools

In the Java world, there are several classic (and free!) tools for profiling. Let’s look at the main ones.

VisualVM

VisualVM is a free tool included with the JDK (starting from JDK 6). It allows you to:

  • Connect to local and remote JVMs.
  • Inspect memory, threads, CPU, and garbage collection.
  • Take a heap dump and analyze it.
  • Profile an application by CPU and memory.

How to launch VisualVM?
It’s usually located in the JDK folder: <path_to_JDK>/bin/jvisualvm
Start it, select your Java process — and you can watch its life like fish in an aquarium (only here the “fish” are objects and threads).

JProfiler, YourKit

These are commercial but very powerful tools. They allow you to:

  • Profile memory, CPU, and threads.
  • Analyze memory snapshots (heap dump).
  • Find leaks, long locks, and slow methods.
  • Integrate with IDEs and CI/CD.

VisualVM is enough to start with, but if you “grow” to larger projects — consider these tools.

Java Flight Recorder (JFR)

JFR is a tool built into the JDK for collecting JVM runtime events. It’s very lightweight, has almost no performance impact, and allows you to collect information about:

  • Method execution time.
  • Garbage collection.
  • Threads, locks, errors.

JFR is great for production, when you cannot slow down the application.

3. Practice: profiling a simple application

Let’s create a mini calculator that can perform long computations and store the operation history (so we have loops, collections, and memory usage).

Code example: “Slow calculator”

import java.util.ArrayList;
import java.util.List;

public class SlowCalculator {
    private final List<String> history = new ArrayList<>();

    public int add(int a, int b) {
        simulateHeavyOperation();
        int result = a + b;
        history.add(a + " + " + b + " = " + result);
        return result;
    }

    public int multiply(int a, int b) {
        simulateHeavyOperation();
        int result = a * b;
        history.add(a + " * " + b + " = " + result);
        return result;
    }

    public List<String> getHistory() {
        return history;
    }

    // Simulation of a "heavy" operation
    private void simulateHeavyOperation() {
        for (int i = 0; i < 5_000_000; i++) {
            Math.sqrt(i);
        }
    }
}

And now — the main class:

public class Main {
    public static void main(String[] args) {
        SlowCalculator calc = new SlowCalculator();
        for (int i = 0; i < 10; i++) {
            calc.add(i, i * 2);
            calc.multiply(i, i + 5);
        }
        System.out.println("Operation history:");
        for (String entry : calc.getHistory()) {
            System.out.println(entry);
        }
    }
}

How do you profile this application?

  1. Compile and run the application.
  2. Open VisualVM (jvisualvm).
  3. Find your process (usually by the class name Main).
  4. Go to the CPU Profiler tab and click Start.
  5. Let the program run (or click the slow button again).
  6. See which methods take the most time.

Question: Which method do you think will be the “heaviest”?
Answer: Of course, simulateHeavyOperation() — it runs a huge loop for 5_000_000 iterations and calls Math.sqrt.

4. Common performance problems

Slow algorithms

The most common reason: a poor choice of algorithm or data structure. For example, searching in a list instead of using a HashMap, or bubble sort instead of quicksort.

Example:

// Slow search
for (String s : list) {
    if (s.equals("target")) {
        // found
    }
}

It’s better to use a Set or Map for fast lookups.

Memory leaks

A memory leak is a situation where objects remain “alive” (there are references to them) even though they are no longer needed. This leads to increased memory consumption and, ultimately, to an OutOfMemoryError.

public class MemoryLeakDemo {
    private static List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            leakyList.add(new byte[1_000_000]); // 1 MB
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        }
    }
}

How to find leaks?
Take a heap dump in VisualVM and see which objects occupy the most memory and why there are references to them.

Excessive object creation

If you create many similar objects in a loop, it not only loads the garbage collector but can also slow down the application.

for (int i = 0; i < 1_000_000; i++) {
    String s = new String("hello"); // bad!
}

It’s better to use constants or the string pool (String pool).

Thread locking

If several threads compete for the same resource (for example, a synchronized method), it can lead to locks and performance degradation.

public synchronized void doWork() {
    // ...
}

How to find them?
On the Threads tab in VisualVM, you can see which threads are “stuck” and why.

5. Approaches to optimization

Measure first, then optimize

The main rule of optimization: Don’t optimize what isn’t slow.

Profile first, find the hot spots, and only then change the code. Sometimes the most “obvious” section of code accounts for only 1 % of the time, while the real “monster” is in a library or some unexpected place.

Using a profiler to find hot spots

A hot spot is a method or piece of code that accounts for the largest share of the application’s runtime.

In VisualVM, you can see this on the CPU Profiler tab:

  • Sort methods by execution time.
  • Look at the stack trace: who calls whom.
  • Remember that sometimes the culprit isn’t your code but a library or even the JDK.

Optimization examples

Example 1: Replacing the algorithm
If you find that most of the time is spent searching a list, replace List with HashSet.

Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
    // fast!
}

Example 2: Reducing allocations
Instead of creating new objects in a loop, use reuse or a StringBuilder.

// Bad:
for (int i = 0; i < 10000; i++) {
    String s = "Result: " + i;
}

// Better:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);
    sb.append("Result: ").append(i);
    String s = sb.toString();
}

Example 3: Caching
If you see that a heavy method is called many times with the same parameters, use a cache.

Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
    return sqrtCache.computeIfAbsent(x, Math::sqrt);
}

6. Demonstration: speeding up our calculator

Problem: simulateHeavyOperation() takes too much time

Step 1. Profile
In VisualVM, it’s clear that almost all the time is spent on Math.sqrt(i) inside the loop of 5_000_000 iterations.

Step 2. Optimize
If it’s just a load simulation — remove it or reduce the number of iterations.
If it’s real business logic — consider whether you can:

  • Cache the result.
  • Use a faster algorithm.
  • Move computations to a separate thread (if it’s not critical for the user).

Optimization example:

private void simulateHeavyOperation() {
    // Was 5_000_000, became 100_000
    for (int i = 0; i < 100_000; i++) {
        Math.sqrt(i);
    }
}

Step 3. Verify the result
Run profiling again — the program runs faster, CPU load has decreased.

7. Visualization: the optimization process

flowchart TD
    A[Application start]
    B["Profiling (VisualVM)"]
    C[Identify bottlenecks]
    D[Code optimization]
    E[Profiling again]
    F[Performance improvement]

    A --> B --> C --> D --> E --> F
    E --> C

8. Common mistakes in profiling and optimization

Error #1: Eyeballing optimization. Developers often start changing code without measuring where the real problem is. As a result — lots of work, minimal benefit.

Error #2: Profiling in “unrealistic” conditions. Profile with data and load that are close to production. Profiling “on an empty place” may not reveal real issues.

Error #3: Ignoring memory leaks. If you don’t look at the heap dump and analyze references, you can go a long time without noticing the program is “bloated” and will soon crash.

Error #4: Chasing microscopic optimizations. Don’t spend days speeding up code that accounts for 0.1 % of the application’s runtime. Tackle the main bottlenecks first.

Error #5: Ignoring threads and synchronization. In multithreaded applications, performance problems are often related not to algorithms but to locks and waits (synchronized, contention).

Error #6: Forgetting to profile after changes. After optimization, be sure to re-check the result: sometimes an “optimization” can even slow things down!

1
Task
JAVA 25 SELF, level 63, lesson 4
Locked
Eco-activist
Eco-activist
1
Task
JAVA 25 SELF, level 63, lesson 4
Locked
Captain Kirk
Captain Kirk
1
Survey/quiz
Logging, level 63, lesson 4
Unavailable
Logging
Logging, monitoring, and profiling
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION