diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java index 892f09c0073c9..a2a1edaaf9594 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/management/SubstrateThreadMXBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, 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 @@ -289,6 +289,14 @@ public ThreadInfo[] dumpAllThreads(boolean lockedMonitors, boolean lockedSynchro return new ThreadInfo[0]; } + @Override + public long getTotalThreadAllocatedBytes() { + if (!isThreadAllocatedMemoryEnabled()) { + return -1; + } + return PlatformThreads.getTotalThreadAllocatedBytes(); + } + @Override public long getThreadAllocatedBytes(long id) { boolean valid = verifyThreadAllocatedMemory(id); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java index 0f8cb1fcb4097..a28fdf72d77ba 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java @@ -141,6 +141,12 @@ public static PlatformThreads singleton() { */ private final UninterruptibleUtils.AtomicInteger unattachedStartedThreads = new UninterruptibleUtils.AtomicInteger(0); + /** Maximum reported total of allocated bytes, retained so reported totals never decrease. */ + private static final UninterruptibleUtils.AtomicLong maxReportedTotalAllocatedBytes = new UninterruptibleUtils.AtomicLong(0); + + /** Bytes allocated by detached threads. Protected by {@link ThreadsLock}. */ + private static long allocatedBytesOfDetachedThreads; + /** The default group for new Threads that are attached without an explicit group. */ final ThreadGroup mainGroup; /** The root group for all threads. */ @@ -241,6 +247,55 @@ public static long getThreadCpuTime(long javaThreadId, boolean includeSystemTime } } + /** + * Returns an approximation of the heap bytes allocated by live and detached threads. + *
+ * TLAB allocations can temporarily be counted twice if a reader sees the updated cumulative + * count before the retired TLAB descriptor is cleared. A later sample can therefore be smaller. + * To keep results from decreasing, this method retains the maximum reported total even after + * the allocating thread exits. This can preserve an overestimate until further allocations make + * up the difference. + *
+ * If the computed total wraps to a negative value, the maximum reported total is returned.
+ * The counters continue wrapping. A later positive total can replace the maximum reported total
+ * if it is larger. The counters do not saturate at {@link Long#MAX_VALUE}.
+ */
+ @BasedOnJDKFile("https://github.com/graalvm/labs-openjdk/blob/jdk-25.0.5.1+0/src/hotspot/share/services/management.cpp#L2093-L2126")
+ public static long getTotalThreadAllocatedBytes() {
+ long totalAllocatedBytes = computeTotalThreadAllocatedBytes();
+
+ /* Unlike HotSpot's MonitoringSupport_lock, an atomic update protects the maximum reported total. */
+ long previousMaxReportedTotalAllocatedBytes;
+ do {
+ previousMaxReportedTotalAllocatedBytes = maxReportedTotalAllocatedBytes.get();
+ if (totalAllocatedBytes <= previousMaxReportedTotalAllocatedBytes) {
+ return previousMaxReportedTotalAllocatedBytes;
+ }
+ } while (!maxReportedTotalAllocatedBytes.compareAndSet(previousMaxReportedTotalAllocatedBytes, totalAllocatedBytes));
+
+ return totalAllocatedBytes;
+ }
+
+ private static long computeTotalThreadAllocatedBytes() {
+ /*
+ * Unlike HotSpot's thread-list handle, our threads lock protects both list removal and
+ * allocation accounting in VMThreads.detachThread. A reader cannot miss a thread between its
+ * removal from the list and the update to the detached-thread allocation total.
+ */
+ ThreadsLock.lockRead();
+ try {
+ long totalAllocatedBytes = allocatedBytesOfDetachedThreads;
+ for (IsolateThread thread = VMThreads.firstThread(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) {
+ long allocatedBytes = Heap.getHeap().getThreadAllocatedMemory(thread);
+ totalAllocatedBytes += allocatedBytes;
+ }
+
+ return totalAllocatedBytes;
+ } finally {
+ ThreadsLock.unlockRead();
+ }
+ }
+
public static void getThreadAllocatedBytes(long[] javaThreadIds, long[] result) {
ThreadsLock.lockRead();
try {
@@ -251,7 +306,6 @@ public static void getThreadAllocatedBytes(long[] javaThreadIds, long[] result)
for (int i = 0; i < javaThreadIds.length; i++) {
if (JavaThreads.getThreadId(javaThread) == javaThreadIds[i]) {
result[i] = Heap.getHeap().getThreadAllocatedMemory(isolateThread);
- break;
}
}
}
@@ -588,6 +642,12 @@ public long getMainThreadRunnerJavaStackSize() {
}
@Uninterruptible(reason = "Thread is detaching and holds the ThreadsLock with exclusive write access.")
+ static void recordAllocatedBytesOfDetachedThread(IsolateThread vmThread) {
+ assert ThreadsLock.hasExclusiveWriteAccess();
+ allocatedBytesOfDetachedThreads += Heap.getHeap().getThreadAllocatedMemory(vmThread);
+ }
+
+ @Uninterruptible(reason = "Thread is detaching or the isolate is being torn down.")
public static void detach(IsolateThread vmThread) {
Thread thread = currentThread.get(vmThread);
if (thread != null) {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java
index 0cc2e23f9ef82..ab1fc797037d2 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java
@@ -422,6 +422,9 @@ protected void detachThread(IsolateThread thread) {
try {
removeFromThreadList(thread);
PlatformThreads.detach(thread);
+
+ /* Record the thread's allocated bytes before heap detachment releases its allocation buffers. */
+ PlatformThreads.recordAllocatedBytesOfDetachedThread(thread);
Heap.getHeap().detachThread(thread);
/*
diff --git a/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/core/heap/Heap.java b/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/core/heap/Heap.java
index 48804f0b38450..e974c5ff8ecad 100644
--- a/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/core/heap/Heap.java
+++ b/substratevm/src/com.oracle.svm.guest.staging/src/com/oracle/svm/guest/staging/core/heap/Heap.java
@@ -67,9 +67,10 @@ protected Heap() {
public abstract void attachThread(IsolateThread isolateThread);
/**
- * Notifies the heap that a thread will be detached from the VM. This allows to cleanup
- * heap-specific resources, e.g., the TLAB. This method is called for every thread except the
- * main thread (i.e., the one that maps the image heap).
+ * Notifies the heap that a thread will be detached from the VM. This allows cleaning up
+ * heap-specific resources, e.g., the TLAB. During isolate teardown, this method is not called
+ * for the last attached thread. Its remaining heap-specific resources are released by
+ * {@link #tearDown}.
*/
@Uninterruptible(reason = "Current thread holds the ThreadsLock with exclusive write access.")
public abstract void detachThread(IsolateThread isolateThread);
@@ -128,7 +129,13 @@ public void visitLoadedClasses(Consumer
+ * Once this method is called, only the thread tearing down the isolate is still attached.
+ * The implementation must also release the heap-specific resources of this last thread, as
+ * {@link #detachThread} does for other threads.
+ */
@Uninterruptible(reason = "Tear-down in progress.")
public abstract boolean tearDown();