@@ -352,6 +352,13 @@ void ReferenceChainTracker::startThread() {
352352 // previously call pthread_kill() on a still value-initialized (0) or
353353 // stale/joined pthread_t, which is undefined behavior. Publishing _thread
354354 // before _running closes that window.
355+ // Reset from any previous stopThread() call - a dynamic-attach profiler
356+ // can go through multiple start()/stop() cycles in one JVM lifetime (this
357+ // class's own start()/stop() header comments), and a stale abort request
358+ // left set from the prior cycle would make heapReferenceCallback() abort
359+ // this new cycle's very first pass instantly.
360+ _abort_pass_requested.store (false , std::memory_order_relaxed);
361+
355362 pthread_t thread;
356363 if (pthread_create (&thread, NULL , threadEntry, this ) != 0 ) {
357364 Log::warn (" Unable to create ReferenceChains BFS thread" );
@@ -366,6 +373,11 @@ void ReferenceChainTracker::stopThread() {
366373 return ;
367374 }
368375 _running = false ;
376+ // Ask any in-flight JVMTI FollowReferences walk (heapReferenceCallback())
377+ // to abort at its next callback invocation - set before pthread_kill()
378+ // below, since that signal alone cannot interrupt a call already inside
379+ // the JVM/JVMTI implementation.
380+ _abort_pass_requested.store (true , std::memory_order_relaxed);
369381 // Same wake-then-join shape as BaseWallClock::stop() (wallClock.cpp:324-333):
370382 // pthread_kill(WAKEUP_SIGNAL) interrupts threadLoop()'s OS::sleep() early
371383 // (WAKEUP_SIGNAL/SIGIO is installed with a no-op handler unconditionally
@@ -601,6 +613,63 @@ void ReferenceChainTracker::restartSearch() {
601613 // _search_start_ns again on this restarted search's own first pass.
602614}
603615
616+ void ReferenceChainTracker::resetSearchStateForTest (jvmtiEnv *jvmti,
617+ JNIEnv *jni) {
618+ // Every field touched below is otherwise only ever mutated by the BFS
619+ // thread itself (threadLoop()/runPass()/pollWatchedTargets()) - without
620+ // stopping it first, a pass already in flight on that thread can observe
621+ // this reset only partially, or overwrite it right back (e.g. finish a
622+ // pass that was already headed for SearchState::ABANDONED after this
623+ // method has just forced SearchState::RUNNING below), a race found in
624+ // practice, not just in theory. stopThread() (now that it can abort an
625+ // in-flight JVMTI walk promptly - see its own comment) makes this a cheap,
626+ // clean stop/reset/restart rather than an indefinite wait.
627+ stopThread ();
628+
629+ // Clear every live tag this search still holds before resetting - the
630+ // same ordering restartSearch() itself requires (its own assert), so a
631+ // stale tag from whatever search a previous test left running cannot
632+ // collide with the fresh search's own tags once _next_tag is rewound
633+ // below.
634+ if (jvmti != nullptr && jni != nullptr ) {
635+ releaseSearchTags (jvmti, jni);
636+ }
637+ _tags_released = true ;
638+
639+ _pain_budget.spend (_search_pain_ms);
640+ _search_pain_ms = 0 ;
641+
642+ if (_frontier != nullptr ) {
643+ _frontier->resetForRestart ();
644+ }
645+ _next_tag = 1 ;
646+
647+ _search_started = false ;
648+ store (_search_state, (u8 )SearchState::RUNNING );
649+ store (_abandon_reason, (u8 )SearchAbandonReason::NONE );
650+ store (_search_start_ns, (u64 )0 );
651+ _expand_cursor = 1 ;
652+ _last_pass_gc_finish_epoch = 0 ;
653+ store (_last_pass_ns, (u64 )0 );
654+ store (_passes_run, 0 );
655+
656+ // Unlike restartSearch(), which leaves these for pollWatchedTargets() to
657+ // notice the _search_start_ns change and clear naturally on its own next
658+ // call, this reset must take effect immediately - there is no next real
659+ // pass here to trigger that self-clear.
660+ _emitted_target_tags.clear ();
661+ _emitted_search_start_ns = 0 ;
662+
663+ _pending_chain_events_lock.lock ();
664+ _pending_chain_events.clear ();
665+ _pending_chain_events_lock.unlock ();
666+
667+ // Restart the BFS thread against this freshly reset state - startThread()
668+ // itself clears _abort_pass_requested, so the new thread's very first
669+ // pass is not instantly aborted by the flag stopThread() just set above.
670+ startThread ();
671+ }
672+
604673jlong ReferenceChainTracker::tagObject (jvmtiEnv *jvmti, jobject obj) {
605674 assert (!t_inGCCallback &&
606675 " SetTag is a JVMTI Heap-category call and must not be made from "
@@ -797,6 +866,21 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
797866 jlong *referrer_tag_ptr, jint length, void *user_data) {
798867 PassContext *ctx = (PassContext *)user_data;
799868
869+ if (ctx->tracker ->_abort_pass_requested .load (std::memory_order_relaxed)) {
870+ // stopThread() has set this right before pthread_kill()/pthread_join() -
871+ // see that method's own comment. pthread_kill(WAKEUP_SIGNAL) only
872+ // interrupts threadLoop()'s OS::sleep(); it cannot interrupt an
873+ // in-flight JVMTI FollowReferences call, so without this check
874+ // pthread_join() would block until this pass's walk finishes on its own
875+ // - potentially the whole reachable graph, well past any caller's
876+ // shutdown timeout. Treat it exactly like an ordinary budget exhaustion
877+ // (ctx->truncated = true): this pass ends early and the search stays
878+ // non-terminal - fine, since the tracker is shutting down and simply
879+ // never resumes it.
880+ ctx->truncated = true ;
881+ return JVMTI_VISIT_ABORT ;
882+ }
883+
800884 if (*tag_ptr < 0 || reference_kind == JVMTI_HEAP_REFERENCE_CLASS ||
801885 reference_kind == JVMTI_HEAP_REFERENCE_SYSTEM_CLASS ) {
802886 // Referee is a class object - either already tagged negative by
@@ -1133,6 +1217,10 @@ bool ReferenceChainTracker::runPass(jvmtiEnv *jvmti, JNIEnv *jni,
11331217
11341218 resolveLoadedClasses (jvmti, jni);
11351219
1220+ TEST_LOG (" ReferenceChainTracker::runPass starting JVMTI walk: "
1221+ " search_started=%d frontierSize=%zu" ,
1222+ _search_started, _frontier != nullptr ? _frontier->size () : (size_t )0 );
1223+
11361224 int edges_admitted = 0 ;
11371225 bool truncated = false ;
11381226 bool frontier_cap_hit = false ;
@@ -1462,8 +1550,13 @@ void ReferenceChainTracker::enqueueChainEvent(ReferenceChainEvent &&event) {
14621550 // "dropped-event-without-counter" review lens).
14631551 _pending_chain_events.erase (_pending_chain_events.begin ());
14641552 Counters::increment (REFERENCE_CHAIN_EVENTS_DROPPED );
1553+ TEST_LOG (" ReferenceChainTracker::enqueueChainEvent dropped oldest queued event, "
1554+ " queue_size=%d (at MAX_PENDING_CHAIN_EVENTS=%d)" ,
1555+ (int )_pending_chain_events.size (), MAX_PENDING_CHAIN_EVENTS );
14651556 }
14661557 _pending_chain_events.push_back (std::move (event));
1558+ TEST_LOG (" ReferenceChainTracker::enqueueChainEvent queued, queue_size=%d" ,
1559+ (int )_pending_chain_events.size ());
14671560 _pending_chain_events_lock.unlock ();
14681561}
14691562
@@ -1480,4 +1573,6 @@ void ReferenceChainTracker::drainPendingChainEvents(
14801573 _pending_chain_events.clear ();
14811574 }
14821575 _pending_chain_events_lock.unlock ();
1576+ TEST_LOG (" ReferenceChainTracker::drainPendingChainEvents drained=%d" ,
1577+ (int )out->size ());
14831578}
0 commit comments