@@ -243,6 +243,13 @@ Error ReferenceChainTracker::start(Arguments &args) {
243243
244244 _hop_cap = args._reference_chains_hop_cap ;
245245 _budget = args._reference_chains_budget ;
246+ // 0 (unset) falls back to _budget - see this field's own comment
247+ // (referenceChains.h) for why the first pass needs its own, larger
248+ // ceiling rather than sharing _budget/_effective_budget with every later
249+ // pass.
250+ _first_pass_budget = args._reference_chains_first_pass_budget > 0
251+ ? args._reference_chains_first_pass_budget
252+ : _budget;
246253 _ttl_ms = args._reference_chains_ttl_ms ;
247254
248255 // Pause-time pacing controller: (re)seed the controller's ceiling and the
@@ -603,7 +610,7 @@ void ReferenceChainTracker::restartSearch() {
603610 store (_search_state, (u8 )SearchState::RUNNING );
604611 store (_abandon_reason, (u8 )SearchAbandonReason::NONE );
605612 store (_search_start_ns, (u64 )0 );
606- _expand_cursor = 1 ;
613+ _pending_expand. clear () ;
607614 _last_pass_gc_finish_epoch = 0 ;
608615 store (_last_pass_ns, (u64 )0 );
609616 store (_passes_run, 0 );
@@ -648,7 +655,7 @@ void ReferenceChainTracker::resetSearchStateForTest(jvmtiEnv *jvmti,
648655 store (_search_state, (u8 )SearchState::RUNNING );
649656 store (_abandon_reason, (u8 )SearchAbandonReason::NONE );
650657 store (_search_start_ns, (u64 )0 );
651- _expand_cursor = 1 ;
658+ _pending_expand. clear () ;
652659 _last_pass_gc_finish_epoch = 0 ;
653660 store (_last_pass_ns, (u64 )0 );
654661 store (_passes_run, 0 );
@@ -760,6 +767,27 @@ jlong ReferenceChainTracker::tagAsRootForTest(jvmtiEnv *jvmti, JNIEnv *jni,
760767
761768void ReferenceChainTracker::resolveLoadedClasses (jvmtiEnv *jvmti,
762769 JNIEnv *jni) {
770+ // Profiler::start() resets the class-name StringDictionary
771+ // (_class_map.clearAll(), profiler.cpp) whenever `reset || _start_time ==
772+ // 0` - which restarts its id namespace at 1, but does NOT touch any
773+ // class's JVMTI-level class-object tag (JVM-level state, unrelated to our
774+ // dictionary). Detect that reset via the dictionary's own generation
775+ // counter and drop every id this table cached from the now-gone
776+ // generation before the scan below - see _last_class_map_generation's own
777+ // comment (referenceChains.h) for why leaving them in place would keep
778+ // resolving heap references to the wrong (or nonexistent) class name.
779+ u64 current_generation = Profiler::instance ()->classMap ()->generation ();
780+ bool class_map_reset = current_generation != _last_class_map_generation;
781+ if (class_map_reset) {
782+ _class_tags.clear ();
783+ // Force the scan below to run even if GetLoadedClasses()'s count happens
784+ // to match the last-seen count - -1 can never equal `class_count`
785+ // (always >= 0), unlike 0 which is a legitimate "no classes loaded yet"
786+ // starting value.
787+ _last_resolved_class_count = -1 ;
788+ _last_class_map_generation = current_generation;
789+ }
790+
763791 jclass *classes = nullptr ;
764792 jint class_count = 0 ;
765793 if (jvmti->GetLoadedClasses (&class_count, &classes) != JVMTI_ERROR_NONE ||
@@ -793,12 +821,18 @@ void ReferenceChainTracker::resolveLoadedClasses(jvmtiEnv *jvmti,
793821 for (jint i = 0 ; i < class_count; i++) {
794822 jclass klass = classes[i];
795823 jlong tag = 0 ;
796- if (jvmti->GetTag (klass, &tag) == JVMTI_ERROR_NONE && tag == 0 ) {
797- // Not yet tagged (by us or a prior pass) - resolve its name now, via
798- // the same GetClassSignature + normalizeClassSignature +
799- // Profiler::lookupClass sequence ObjectSampler::recordAllocation()
800- // already uses (objectSampler.cpp:76-90), reused rather than
801- // re-derived.
824+ // Resolve if not yet tagged (ordinary case: a newly-loaded class), or
825+ // unconditionally on a class-map reset (class_map_reset above) - a
826+ // class already tagged from a prior generation still carries that same
827+ // JVMTI tag (untouched by clearAll()), but the dictionary id it used to
828+ // map to is gone, so its name must be re-resolved into the new
829+ // generation too.
830+ if (jvmti->GetTag (klass, &tag) == JVMTI_ERROR_NONE &&
831+ (tag == 0 || class_map_reset)) {
832+ // Resolve its name now, via the same GetClassSignature +
833+ // normalizeClassSignature + Profiler::lookupClass sequence
834+ // ObjectSampler::recordAllocation() already uses
835+ // (objectSampler.cpp:76-90), reused rather than re-derived.
802836 char *class_name = nullptr ;
803837 if (jvmti->GetClassSignature (klass, &class_name, nullptr ) ==
804838 JVMTI_ERROR_NONE &&
@@ -809,8 +843,12 @@ void ReferenceChainTracker::resolveLoadedClasses(jvmtiEnv *jvmti,
809843 &name_len)) {
810844 int id = Profiler::instance ()->lookupClass (name_slice, name_len);
811845 if (id != -1 ) {
812- jlong class_tag = nextClassTag ();
813- if (jvmti->SetTag (klass, class_tag) == JVMTI_ERROR_NONE ) {
846+ // Reuse the existing tag if this class was already tagged by a
847+ // prior generation - only the resolved id needs refreshing,
848+ // not the tag identity heapReferenceCallback() keys off of.
849+ jlong class_tag = tag != 0 ? tag : nextClassTag ();
850+ if (tag != 0 ||
851+ jvmti->SetTag (klass, class_tag) == JVMTI_ERROR_NONE ) {
814852 _class_tags.insert (class_tag, (u32 )id);
815853 }
816854 }
@@ -965,6 +1003,10 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
9651003 }
9661004 *tag_ptr = tag;
9671005 ctx->edges_admitted ++;
1006+ // Queue for expandFrontier()/markAllFrontierExpanded() - see
1007+ // _pending_expand's own declaration comment for why this replaces a
1008+ // scan over the admitted range.
1009+ ctx->tracker ->_pending_expand .push_back (tag);
9681010 }
9691011
9701012 return JVMTI_VISIT_OBJECTS ;
@@ -975,15 +1017,10 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
9751017// ---------------------------------------------------------------------------
9761018
9771019void ReferenceChainTracker::markAllFrontierExpanded () {
978- jlong scan_limit = _frontier->size ();
979- for (jlong tag = 1 ; tag <= scan_limit; tag++) {
980- FrontierEntry entry{};
981- if (_frontier->lookup (tag, &entry) &&
982- entry.state == FrontierEntryState::FRONTIER ) {
983- _frontier->markExpanded (tag);
984- }
1020+ while (!_pending_expand.empty ()) {
1021+ _frontier->markExpanded (_pending_expand.front ());
1022+ _pending_expand.pop_front ();
9851023 }
986- _expand_cursor = scan_limit + 1 ;
9871024}
9881025
9891026void ReferenceChainTracker::expandFrontier (jvmtiEnv *jvmti, JNIEnv *jni,
@@ -1012,29 +1049,26 @@ void ReferenceChainTracker::expandFrontier(jvmtiEnv *jvmti, JNIEnv *jni,
10121049 while (!ctx.truncated && progress) {
10131050 progress = false ;
10141051
1015- // Re-read size() every outer iteration: expanding one entry can insert
1016- // new FRONTIER children with higher tags, extending the range this loop
1017- // needs to cover before this pass's budget runs out (design doc
1018- // Algorithm step 3/4 - admitting them immediately, rather than waiting
1019- // for a whole extra pass, is a strictly tighter use of the budget this
1020- // call already has).
1021- jlong scan_limit = _frontier->size ();
1022- if (_expand_cursor > scan_limit) {
1052+ if (_pending_expand.empty ()) {
10231053 break ; // nothing pending
10241054 }
10251055
1026- std::vector<jlong> candidate_tags;
1027- for (jlong tag = _expand_cursor; tag <= scan_limit; tag++) {
1028- FrontierEntry entry{};
1029- if (_frontier->lookup (tag, &entry) &&
1030- entry.state == FrontierEntryState::FRONTIER ) {
1031- candidate_tags.push_back (tag);
1032- }
1033- }
1034- if (candidate_tags.empty ()) {
1035- _expand_cursor = scan_limit + 1 ;
1036- break ; // caught up; nothing pending
1037- }
1056+ // Snapshot at most `budget` pending tags per iteration, not the whole
1057+ // queue: _pending_expand can hold far more than this call could ever
1058+ // admit (e.g. every hop_cap-limited entry from a huge one-shot first
1059+ // pass), and GetObjectsWithTags()'s own cost scales with the batch size
1060+ // handed to it - resolving the entire backlog up front just to abandon
1061+ // most of it once `budget` is hit would reintroduce the same
1062+ // admitted-range-sized cost this queue was meant to avoid. Entries
1063+ // expanded below can push new FRONTIER children onto the back of
1064+ // _pending_expand (design doc Algorithm step 3/4 - admitting them
1065+ // immediately, rather than waiting for a whole extra pass, is a
1066+ // strictly tighter use of the budget this call already has); those wait
1067+ // for a later outer-loop iteration.
1068+ size_t batch_size =
1069+ std::min (_pending_expand.size (), (size_t )std::max (budget, 1 ));
1070+ std::vector<jlong> candidate_tags (_pending_expand.begin (),
1071+ _pending_expand.begin () + batch_size);
10381072
10391073 // Design doc Algorithm step 2: "resolve currently-live tagged frontier
10401074 // objects" - batched via one GetObjectsWithTags call rather than one
@@ -1064,14 +1098,18 @@ void ReferenceChainTracker::expandFrontier(jvmtiEnv *jvmti, JNIEnv *jni,
10641098 if (ctx.truncated ) {
10651099 break ; // budget/frontier-cap already hit by a sibling in this batch
10661100 }
1101+ // `tag` is always _pending_expand's current front here: candidate_tags
1102+ // is a front-to-back snapshot, and nothing removes from the front
1103+ // except this loop itself - concurrent admissions (below) only append
1104+ // to the back.
10671105 auto it = live.find (tag);
10681106 if (it == live.end ()) {
10691107 // Design doc Algorithm step 2: "objects that fail to resolve are
10701108 // dropped (dead - free pruning)". The object died between passes;
10711109 // JVMTI already forgot its tag along with it, so there is nothing
10721110 // left to expand and no live object to release a tag on.
10731111 _frontier->clear (tag);
1074- _expand_cursor = tag + 1 ;
1112+ _pending_expand. pop_front () ;
10751113 progress = true ;
10761114 continue ;
10771115 }
@@ -1087,14 +1125,15 @@ void ReferenceChainTracker::expandFrontier(jvmtiEnv *jvmti, JNIEnv *jni,
10871125 if (follow_err != JVMTI_ERROR_NONE || ctx.truncated ) {
10881126 // Either this call failed outright, or ran into the budget/
10891127 // frontier-cap mid-way through this entry's own children. Leave
1090- // `tag` FRONTIER (not EXPANDED) so a later pass retries it -
1091- // already-admitted children stay tagged/in the table, and
1092- // heapReferenceCallback()'s *tag_ptr == 0 check makes re-expanding
1093- // this entry idempotent, so no work is lost, only deferred.
1128+ // `tag` at the front of _pending_expand (still FRONTIER, not
1129+ // EXPANDED) so a later pass retries it - already-admitted children
1130+ // stay tagged/in the table, and heapReferenceCallback()'s
1131+ // *tag_ptr == 0 check makes re-expanding this entry idempotent, so
1132+ // no work is lost, only deferred.
10941133 break ;
10951134 }
10961135 _frontier->markExpanded (tag);
1097- _expand_cursor = tag + 1 ;
1136+ _pending_expand. pop_front () ;
10981137 progress = true ;
10991138 }
11001139
@@ -1237,6 +1276,10 @@ bool ReferenceChainTracker::runPass(jvmtiEnv *jvmti, JNIEnv *jni,
12371276 // unavailable/disabled, so this is a strict upgrade with no behavior change
12381277 // on hosts without a usable timestamp counter.
12391278 u64 pass_wall_ticks = 0 ;
1279+ // Captured before _search_started flips below - see its use at
1280+ // updatePacing() further down for why the first pass's own duration must
1281+ // not feed that controller the same way every later pass's does.
1282+ bool was_first_pass = !_search_started;
12401283
12411284 if (!_search_started) {
12421285 _search_started = true ;
@@ -1246,7 +1289,7 @@ bool ReferenceChainTracker::runPass(jvmtiEnv *jvmti, JNIEnv *jni,
12461289 ctx.tracker = this ;
12471290 ctx.frontier = _frontier;
12481291 ctx.hop_cap = _hop_cap;
1249- ctx.budget = _effective_budget ;
1292+ ctx.budget = _first_pass_budget ;
12501293 ctx.edges_admitted = 0 ;
12511294 ctx.truncated = false ;
12521295 ctx.frontier_cap_hit = false ;
@@ -1320,7 +1363,15 @@ bool ReferenceChainTracker::runPass(jvmtiEnv *jvmti, JNIEnv *jni,
13201363 store (_passes_run, load (_passes_run) + 1 );
13211364 _last_pass_gc_finish_epoch = gcFinishEpoch ();
13221365 store (_last_pass_ns, OS::nanotime ());
1323- updatePacing (pass_wall_ticks);
1366+ if (!was_first_pass) {
1367+ // The first pass spends _first_pass_budget, not _effective_budget - its
1368+ // duration is not a signal about the per-pass cost updatePacing() is
1369+ // trying to regulate (expandFrontier()'s cheap, per-node expansion
1370+ // calls), so feeding it in here would throttle _effective_budget down
1371+ // for every one of those unrelated later passes based on a single,
1372+ // deliberately oversized outlier.
1373+ updatePacing (pass_wall_ticks);
1374+ }
13241375 // Search restart (this class's own header comment): accumulate this
13251376 // pass's own cost toward the running total restartSearch() will spend into
13261377 // _pain_budget once the search reaches a terminal state - same
0 commit comments