Detect and resolve false sharing in Java on Arm Neoverse
Introduction
Understand false sharing in Java
Create and inspect the baseline
Identify the contended cache line
Add @Contended and verify contention padding
Compare baseline and padded runtimes
Next steps
Detect and resolve false sharing in Java on Arm Neoverse
Add @Contended groups to the example
HotSpot’s @Contended annotation asks the Java Virtual Machine (JVM) to isolate an annotated class or
field group by inserting padding into the object layout. Fields with different
group names are separated, reducing the chance that independent writes target
the same cache line. The annotation changes memory layout rather than Java
concurrency semantics, and the extra padding increases each object’s memory
footprint.
Edit FalseSharingDemo.java so that it contains the following complete program:
import java.util.concurrent.CountDownLatch;
import jdk.internal.vm.annotation.Contended;
/** Baseline and padded controls for cache-line false sharing. */
public final class FalseSharingDemo {
private static final long ITERATIONS = 500_000_000L;
interface Counters {
void incrementLeft();
void incrementRight();
long sum();
}
static final class BaselineCounters implements Counters {
volatile long left;
volatile long right;
public void incrementLeft() { left++; }
public void incrementRight() { right++; }
public long sum() { return left + right; }
}
static final class PaddedCounters implements Counters {
@Contended("left") volatile long left;
@Contended("right") volatile long right;
public void incrementLeft() { left++; }
public void incrementRight() { right++; }
public long sum() { return left + right; }
}
public static void main(String[] args) throws Exception {
if (args.length != 1 ||
!(args[0].equals("baseline") || args[0].equals("padded"))) {
throw new IllegalArgumentException("use: baseline | padded");
}
Counters counters = args[0].equals("baseline")
? new BaselineCounters() : new PaddedCounters();
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
Thread left = new Thread(
() -> runWorker(ready, start, counters::incrementLeft),
"left-writer");
Thread right = new Thread(
() -> runWorker(ready, start, counters::incrementRight),
"right-writer");
left.start();
right.start();
ready.await();
long begin = System.nanoTime();
start.countDown();
left.join();
right.join();
double seconds = (System.nanoTime() - begin) / 1_000_000_000.0;
System.out.printf("mode=%s seconds=%.6f sum=%d pid=%d%n",
args[0], seconds, counters.sum(), ProcessHandle.current().pid());
}
private static void runWorker(
CountDownLatch ready, CountDownLatch start, Runnable update) {
try {
ready.countDown();
start.await();
for (long i = 0; i < ITERATIONS; i++) {
update.run();
}
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new RuntimeException(exception);
}
}
}
The changes import @Contended, add PaddedCounters, accept a padded mode,
and select the counter implementation from the command line. The baseline mode
remains available for a like-for-like comparison.
@Contended is an internal HotSpot annotation. Compile with the package export,
then run application annotations with the restriction disabled:
"$javac_bin" \
--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED \
FalseSharingDemo.java
taskset -c 0,1 "$java_bin" \
--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED \
-XX:-RestrictContended FalseSharingDemo padded
Confirm that the output contains mode=padded and sum=1000000000.
Inspect the padded layout
Inspect PaddedCounters with Java Object Layout (JOL) to confirm that HotSpot inserted padding
between the two contention groups:
"$java_bin" -XX:-RestrictContended \
--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED \
-cp jol-cli-0.17-full.jar:. org.openjdk.jol.Main internals \
'FalseSharingDemo$PaddedCounters'
A representative OpenJDK 21 layout is similar to:
FalseSharingDemo$PaddedCounters object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001
8 4 (object header: class) 0x01085290
12 132 (alignment/padding gap)
144 8 long PaddedCounters.left 0
152 128 (alignment/padding gap)
280 8 long PaddedCounters.right 0
Instance size: 288 bytes
Space losses: 260 bytes internal + 0 bytes external = 260 bytes total
With HotSpot’s default 128-byte contention-padding width, the first reported gap combines the original 4-byte alignment gap with 128 bytes of contention padding. Another 128-byte gap separates the two contention groups. The fields move from offsets 16 and 24 in the 32-byte baseline object to offsets 144 and 280 in the 288-byte padded object. The 128-byte inter-field gap prevents the two 8-byte fields from occupying the same 64-byte cache line, at the cost of 256 additional bytes per object. Exact layouts can vary with JVM settings.
Record and compare the padded mode
Record with the same Perf binary, CPUs, JVM, and workload that are used for the baseline:
sudo perf c2c record -o padded.data -- \
taskset -c 0,1 "$java_bin" \
--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED \
-XX:-RestrictContended FalseSharingDemo padded
sudo perf c2c report --stdio -i padded.data | tee padded-c2c.txt
Confirm that the recorded payload prints mode=padded and
sum=1000000000.
Use the same awk match for both reports to display their highest-ranked cache lines together:
top_c2c_line() {
local label="$1"
local report="$2"
awk -v label="$label" -v filename="$report" '
$1 ~ /^[0-9]+$/ && $2 ~ /^0x[[:xdigit:]]+$/ {
print label " report file:", filename
print label " most contended cache line:", $2
print label " report row:", $0
found = 1
exit
}
END {
if (!found) {
print label ": no shared cache-line row found in " filename > "/dev/stderr"
exit 1
}
}' "$report"
}
top_c2c_line baseline baseline-c2c.txt
top_c2c_line padded padded-c2c.txt
The output is similar to:
baseline report file: baseline-c2c.txt
baseline most contended cache line: 0x101019100
baseline report row: 0 0x101019100 0 60983 32.14% 36 36 0 938563 938544 19 0 0 19 0 938508 0 36 0 0 0 0 0
padded report file: padded-c2c.txt
padded most contended cache line: 0xffff005e4e9c5b00
padded report row: 0 0xffff005e4e9c5b00 0 14 9.30% 4 4 0 18 18 0 0 0 0 0 14 0 4 0 0 0 0 0
Confirm that both reports say Cachelines sort on: Peer Snoop before making
the comparison.
You can interpret the example output as follows:
- The baseline’s top line has 36 peer hits, all local, compared with 4 local peer hits for the padded report’s top line. The baseline count is nine times the padded count, equivalent to an approximately 89% reduction.
- The baseline line contains 938,563 sampled records: 938,544 loads and 19 stores. The padded line contains only 18 sampled records, all loads. The baseline also ran longer, so it provided Perf with more sampling time. Don’t interpret the record-count difference as a normalized performance ratio.
32.14%and9.30%are each line’s share of the peer-snoop events in its own report. Because the two percentages have different denominators, compare the absolute peer counts of 36 and 4 rather than subtracting the percentages.
This result is consistent with contention padding removing the original hot counter line. It doesn’t prove that either address belongs to the counter object. The two addresses don’t need to match because they come from separate JVM processes.
The padded report can still contain shared lines from JVM internals,
CountDownLatch, thread coordination, or other runtime activity. Percentages
can also rise when the total number of samples falls, so compare absolute peer
counts rather than relying only on percentages.
HotSpot normally restricts @Contended in application classes. Keep
-XX:-RestrictContended on every padded run. Without it, HotSpot ignores the
padding annotation for this class.
What you’ve accomplished and what’s next
You’ve separated the fields with @Contended, verified the padded layout,
and compared the highest-ranked lines in the baseline and padded Perf C2C
reports.
Next, you’ll use repeated timing pairs to assess the runtime effect while accounting for measurement variability.