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> visitor) { @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public abstract ObjectHeader getObjectHeader(); - /** Tear down the heap and free all allocated virtual memory chunks. */ + /** + * Tears down the heap and frees all allocated virtual memory chunks. + *

+ * 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();