diff --git a/src/hotspot/share/gc/shared/stringdedup/stringDedupProcessor.cpp b/src/hotspot/share/gc/shared/stringdedup/stringDedupProcessor.cpp
index 163800e9abf9..76ef4abf76f6 100644
--- a/src/hotspot/share/gc/shared/stringdedup/stringDedupProcessor.cpp
+++ b/src/hotspot/share/gc/shared/stringdedup/stringDedupProcessor.cpp
@@ -193,10 +193,8 @@ void StringDedup::Processor::run(JavaThread* thread) {
void StringDedup::Processor::log_statistics() {
_total_stat.add(&_cur_stat);
Stat::log_summary(&_cur_stat, &_total_stat);
- if (log_is_enabled(Debug, stringdedup)) {
- _cur_stat.log_statistics(false);
- _total_stat.log_statistics(true);
- Table::log_statistics();
- }
+ _cur_stat.emit_statistics(false /* total */);
+ _total_stat.emit_statistics(true /* total */);
+ Table::log_statistics();
_cur_stat = Stat{};
}
diff --git a/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.cpp b/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.cpp
index 9ce3d0f91646..e67483d64174 100644
--- a/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.cpp
+++ b/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.cpp
@@ -24,6 +24,7 @@
#include "precompiled.hpp"
#include "gc/shared/stringdedup/stringDedupStat.hpp"
+#include "jfr/jfrEvents.hpp"
#include "logging/log.hpp"
#include "utilities/globalDefinitions.hpp"
@@ -92,13 +93,6 @@ static double strdedup_elapsed_param_ms(Tickspan t) {
}
void StringDedup::Stat::log_summary(const Stat* last_stat, const Stat* total_stat) {
- double total_deduped_bytes_percent = 0.0;
-
- if (total_stat->_new_bytes > 0) {
- // Avoid division by zero
- total_deduped_bytes_percent = percent_of(total_stat->_deduped_bytes, total_stat->_new_bytes);
- }
-
log_info(stringdedup)(
"Concurrent String Deduplication "
"%zu (inspected), "
@@ -110,7 +104,7 @@ void StringDedup::Stat::log_summary(const Stat* last_stat, const Stat* total_sta
last_stat->_inspected,
last_stat->_new, STRDEDUP_BYTES_PARAM(last_stat->_new_bytes),
last_stat->_deduped, STRDEDUP_BYTES_PARAM(last_stat->_deduped_bytes),
- total_deduped_bytes_percent,
+ percent_of(total_stat->_deduped_bytes, total_stat->_new_bytes),
STRDEDUP_BYTES_PARAM(total_stat->_deduped_bytes), STRDEDUP_BYTES_PARAM(total_stat->_new_bytes),
strdedup_elapsed_param_ms(last_stat->_process_elapsed),
strdedup_elapsed_param_ms(last_stat->_active_elapsed));
@@ -213,7 +207,7 @@ void StringDedup::Stat::log_times(const char* prefix) const {
}
}
-void StringDedup::Stat::log_statistics(bool total) const {
+void StringDedup::Stat::log_statistics() const {
double known_percent = percent_of(_known, _inspected);
double known_shared_percent = percent_of(_known_shared, _inspected);
double new_percent = percent_of(_new, _inspected);
@@ -221,7 +215,6 @@ void StringDedup::Stat::log_statistics(bool total) const {
double deduped_bytes_percent = percent_of(_deduped_bytes, _new_bytes);
double replaced_percent = percent_of(_replaced, _new);
double deleted_percent = percent_of(_deleted, _new);
- log_times(total ? "Total" : "Last");
log_debug(stringdedup)(" Inspected: %12zu", _inspected);
log_debug(stringdedup)(" Known: %12zu(%5.1f%%)", _known, known_percent);
log_debug(stringdedup)(" Shared: %12zu(%5.1f%%)", _known_shared, known_shared_percent);
@@ -234,3 +227,40 @@ void StringDedup::Stat::log_statistics(bool total) const {
log_debug(stringdedup)(" Skipped: %zu (dead), %zu (incomplete), %zu (shared)",
_skipped_dead, _skipped_incomplete, _skipped_shared);
}
+
+void StringDedup::Stat::emit_statistics(bool total) const {
+ if (log_is_enabled(Debug, stringdedup)) {
+ log_times(total ? "Total" : "Last");
+ log_statistics();
+ }
+
+ if (total) {
+ // Send only JFR events about the last stats
+ return;
+ }
+
+ EventStringDeduplication e;
+ if (e.should_commit()) {
+ e.set_starttime(_active_start);
+ Ticks active_end = _active_start;
+ active_end += _active_elapsed;
+ e.set_endtime(active_end);
+
+ e.set_inspected(_inspected);
+ e.set_known(_known);
+ e.set_shared(_known_shared);
+ e.set_newStrings(_new);
+ e.set_newSize(_new_bytes);
+ e.set_replaced(_replaced);
+ e.set_deleted(_deleted);
+ e.set_deduplicated(_deduped);
+ e.set_deduplicatedSize(_deduped_bytes);
+ e.set_skippedDead(_skipped_dead);
+ e.set_skippedIncomplete(_skipped_incomplete);
+ e.set_skippedShared(_skipped_shared);
+ e.set_processing(_process_elapsed);
+ e.set_tableResize(_resize_table_elapsed);
+ e.set_tableCleanup(_cleanup_table_elapsed);
+ e.commit();
+ }
+}
diff --git a/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.hpp b/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.hpp
index db753af3be5b..fb864ab34ab6 100644
--- a/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.hpp
+++ b/src/hotspot/share/gc/shared/stringdedup/stringDedupStat.hpp
@@ -71,6 +71,7 @@ class StringDedup::Stat {
void report_phase_end(const char* phase, Tickspan* elapsed);
void log_times(const char* prefix) const;
+ void log_statistics() const;
public:
Stat();
@@ -148,7 +149,7 @@ class StringDedup::Stat {
void report_active_end();
void add(const Stat* const stat);
- void log_statistics(bool total) const;
+ void emit_statistics(bool total) const;
static void log_summary(const Stat* last_stat, const Stat* total_stat);
};
diff --git a/src/hotspot/share/gc/shared/stringdedup/stringDedupTable.cpp b/src/hotspot/share/gc/shared/stringdedup/stringDedupTable.cpp
index ac8fc0759caf..58ab7584d50d 100644
--- a/src/hotspot/share/gc/shared/stringdedup/stringDedupTable.cpp
+++ b/src/hotspot/share/gc/shared/stringdedup/stringDedupTable.cpp
@@ -724,6 +724,10 @@ void StringDedup::Table::verify() {
}
void StringDedup::Table::log_statistics() {
+ if (!log_is_enabled(Debug, stringdedup)) {
+ return;
+ }
+
size_t dead_count;
int dead_state;
{
diff --git a/src/hotspot/share/jfr/metadata/metadata.xml b/src/hotspot/share/jfr/metadata/metadata.xml
index 80ea101a7c96..ac34050d9341 100644
--- a/src/hotspot/share/jfr/metadata/metadata.xml
+++ b/src/hotspot/share/jfr/metadata/metadata.xml
@@ -1178,6 +1178,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/jdk.jfr/share/conf/jfr/default.jfc b/src/jdk.jfr/share/conf/jfr/default.jfc
index ee03f66162e5..210f0296b9a9 100644
--- a/src/jdk.jfr/share/conf/jfr/default.jfc
+++ b/src/jdk.jfr/share/conf/jfr/default.jfc
@@ -450,6 +450,11 @@
true
+
+ true
+ 0 ms
+
+
true
diff --git a/src/jdk.jfr/share/conf/jfr/profile.jfc b/src/jdk.jfr/share/conf/jfr/profile.jfc
index bdd148121c14..b51384503b4f 100644
--- a/src/jdk.jfr/share/conf/jfr/profile.jfc
+++ b/src/jdk.jfr/share/conf/jfr/profile.jfc
@@ -450,6 +450,11 @@
true
+
+ true
+ 0 ms
+
+
true
diff --git a/test/jdk/jdk/jfr/event/gc/detailed/TestStringDeduplicationEvent.java b/test/jdk/jdk/jfr/event/gc/detailed/TestStringDeduplicationEvent.java
new file mode 100644
index 000000000000..3221e76b4d29
--- /dev/null
+++ b/test/jdk/jdk/jfr/event/gc/detailed/TestStringDeduplicationEvent.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jfr.event.gc.detailed;
+
+import java.lang.reflect.Field;
+import java.lang.management.ManagementFactory;
+import java.lang.management.GarbageCollectorMXBean;
+import java.util.List;
+import java.util.ArrayList;
+
+import jdk.jfr.consumer.RecordingStream;
+import jdk.test.lib.jfr.EventNames;
+import jdk.test.whitebox.WhiteBox;
+
+/**
+ * @test id=Serial
+ * @requires vm.flagless
+ * @requires vm.hasJFR
+ * @requires vm.gc.Serial
+ * @library /test/lib /test/jdk
+ * @build jdk.test.whitebox.WhiteBox
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
+ * @run main/othervm -Xbootclasspath/a:. --add-opens=java.base/java.lang=ALL-UNNAMED
+ * -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
+ * -ea
+ * -XX:+UseSerialGC
+ * -XX:+UseStringDeduplication
+ * -XX:StringDeduplicationAgeThreshold=1
+ * -Xlog:stringdedup*=debug
+ * jdk.jfr.event.gc.detailed.TestStringDeduplicationEvent
+ */
+
+/**
+ * @test id=Parallel
+ * @requires vm.flagless
+ * @requires vm.hasJFR
+ * @requires vm.gc.Parallel
+ * @library /test/lib /test/jdk
+ * @build jdk.test.whitebox.WhiteBox
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
+ * @run main/othervm -Xbootclasspath/a:. --add-opens=java.base/java.lang=ALL-UNNAMED
+ * -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
+ * -ea
+ * -XX:+UseParallelGC
+ * -XX:+UseStringDeduplication
+ * -XX:StringDeduplicationAgeThreshold=1
+ * -Xlog:stringdedup*=debug
+ * jdk.jfr.event.gc.detailed.TestStringDeduplicationEvent
+ */
+
+/**
+ * @test id=G1
+ * @requires vm.flagless
+ * @requires vm.hasJFR
+ * @requires vm.gc.G1
+ * @library /test/lib /test/jdk
+ * @build jdk.test.whitebox.WhiteBox
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
+ * @run main/othervm -Xbootclasspath/a:. --add-opens=java.base/java.lang=ALL-UNNAMED
+ * -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
+ * -ea
+ * -XX:+UseG1GC
+ * -XX:+UseStringDeduplication
+ * -XX:StringDeduplicationAgeThreshold=1
+ * -Xlog:stringdedup*=debug
+ * jdk.jfr.event.gc.detailed.TestStringDeduplicationEvent
+ */
+
+/**
+ * @test id=Z
+ * @requires vm.flagless
+ * @requires vm.hasJFR
+ * @requires vm.gc.Z
+ * @library /test/lib /test/jdk
+ * @build jdk.test.whitebox.WhiteBox
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
+ * @run main/othervm -Xbootclasspath/a:. --add-opens=java.base/java.lang=ALL-UNNAMED
+ * -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
+ * -ea
+ * -XX:+UseZGC
+ * -XX:+UseStringDeduplication
+ * -XX:StringDeduplicationAgeThreshold=1
+ * -Xlog:stringdedup*=debug
+ * jdk.jfr.event.gc.detailed.TestStringDeduplicationEvent
+ */
+
+/**
+ * @test id=Shenandoah
+ * @requires vm.flagless
+ * @requires vm.hasJFR
+ * @requires vm.gc.Shenandoah
+ * @library /test/lib /test/jdk
+ * @build jdk.test.whitebox.WhiteBox
+ * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
+ * @run main/othervm -Xbootclasspath/a:. --add-opens=java.base/java.lang=ALL-UNNAMED
+ * -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
+ * -ea
+ * -XX:+UseShenandoahGC
+ * -XX:+UseStringDeduplication
+ * -XX:StringDeduplicationAgeThreshold=1
+ * -Xlog:stringdedup*=debug
+ * jdk.jfr.event.gc.detailed.TestStringDeduplicationEvent
+ */
+
+public class TestStringDeduplicationEvent {
+ private static Field valueField;
+
+ static {
+ try {
+ valueField = String.class.getDeclaredField("value");
+ valueField.setAccessible(true);
+ } catch (Exception exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ boolean zgc = isZgc();
+
+ try (RecordingStream recording = new RecordingStream()) {
+ recording.enable(EventNames.StringDeduplication);
+ recording.onEvent(EventNames.StringDeduplication, e -> recording.close());
+ recording.startAsync();
+
+ String base = TestStringDeduplicationEvent.class.getSimpleName();
+ String duplicate = new StringBuilder(base).toString();
+ assert(getValue(base) != getValue(duplicate));
+
+ if (zgc) {
+ // ZGC only triggers string deduplications from major collections
+ WhiteBox.getWhiteBox().fullGC();
+ } else {
+ WhiteBox.getWhiteBox().youngGC();
+ }
+
+ recording.awaitTermination();
+ }
+ }
+
+ private static Object getValue(String string) {
+ try {
+ return valueField.get(string);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isZgc() {
+ List gcs = ManagementFactory.getGarbageCollectorMXBeans();
+ return gcs.getFirst().getName().contains("ZGC");
+ }
+}
diff --git a/test/lib/jdk/test/lib/jfr/EventNames.java b/test/lib/jdk/test/lib/jfr/EventNames.java
index ba53f9598df5..bc595d7aba2b 100644
--- a/test/lib/jdk/test/lib/jfr/EventNames.java
+++ b/test/lib/jdk/test/lib/jfr/EventNames.java
@@ -154,6 +154,7 @@ public class EventNames {
public static final String GCLocker = PREFIX + "GCLocker";
public static final String SystemGC = PREFIX + "SystemGC";
public static final String GCCPUTime = PREFIX + "GCCPUTime";
+ public static final String StringDeduplication = PREFIX + "StringDeduplication";
// Compiler
public static final String Compilation = PREFIX + "Compilation";