@@ -37,10 +37,18 @@ void LivenessTracker::cleanup_table(bool forced) {
3737 forced, _gc_generations, (unsigned long long )current,
3838 (unsigned long long )target_gc_epoch, _table_size);
3939
40- if ((target_gc_epoch == _last_gc_epoch ||
41- !__atomic_compare_exchange_n (&_last_gc_epoch, ¤t,
42- target_gc_epoch, false , __ATOMIC_RELAXED, __ATOMIC_RELAXED)) &&
43- !forced) {
40+ // is_epoch_owner is true iff this call is the one that moves _last_gc_epoch
41+ // to target_gc_epoch - i.e. the first cleanup_table() call (forced or not)
42+ // to observe this particular GC epoch transition. Population accounting
43+ // below is gated on this rather than on !forced, so a forced (table-
44+ // overflow) sweep still folds one sample per genuinely new epoch instead
45+ // of either skipping it entirely or double-counting the same epoch across
46+ // repeated forced sweeps.
47+ bool is_epoch_owner = target_gc_epoch != current &&
48+ __atomic_compare_exchange_n (&_last_gc_epoch, ¤t, target_gc_epoch,
49+ false , __ATOMIC_RELAXED, __ATOMIC_RELAXED);
50+
51+ if (!is_epoch_owner && !forced) {
4452 // if the last processed GC epoch hasn't changed, or if we failed to update
4553 // it, there's nothing to do
4654 TEST_LOG (" LivenessTracker::cleanup_table early-exit: epoch unchanged and not forced" );
@@ -69,35 +77,52 @@ void LivenessTracker::cleanup_table(bool forced) {
6977 }
7078 _table[target].age += epoch_diff;
7179
72- if (_gc_generations && !forced ) {
80+ if (_gc_generations && is_epoch_owner ) {
7381 // Per-klass population tracking (design doc's Open Question 3) -
74- // gated on _gc_generations so this new cost (a GetObjectClass +
75- // Class.getName() + StringDictionary lookup per surviving entry,
76- // previously paid only at JFR-flush time, see flush_table() below)
77- // is paid only when the caller actually asked for generation/
78- // survival-shaped data (arguments.cpp:223-227,244), not for every
79- // liveness-tracking session. Also gated on !forced: track()'s
80- // table-overflow branch calls cleanup_table(true) synchronously
81- // from the allocation-sampling call stack (JVMTI SampledObjectAlloc
82- // callback), and this class resolution must stay off that path -
83- // see this file's header comment on _klass_population ("never from
84- // track()"). The non-forced callers (flush_table(), stop()) only
85- // run on the JFR-flush/profiler-stop cadence, never from track().
86- jobject ref = env->NewLocalRef (_table[target].ref );
87- if (ref != nullptr ) {
88- u32 klass_id = resolveKlassId (env, ref);
89- if (klass_id != 0 ) {
90- accumulateKlassCount (klass_id, _table[target].ref );
91- // Cache the resolution: flush_table() runs its own
92- // GetObjectClass+Class.getName()+lookupClass() sequence for
93- // every surviving entry immediately after cleanup_table()
94- // returns (flush_table() always calls cleanup_table() first),
95- // which would otherwise repeat this exact JNI round-trip for
96- // the same object. An object's class is immutable, so this
97- // value stays valid for flush_table()'s read below.
98- _table[target].cached_klass_id = klass_id;
82+ // gated on _gc_generations so this new cost is paid only when the
83+ // caller actually asked for generation/survival-shaped data
84+ // (arguments.cpp:223-227,244), not for every liveness-tracking
85+ // session. Gated on is_epoch_owner (not !forced) so a forced
86+ // (table-overflow) sweep still contributes one population sample
87+ // per genuinely new GC epoch instead of silently dropping it.
88+ u32 klass_id = 0 ;
89+ if (!forced) {
90+ // GetObjectClass + Class.getName() + StringDictionary lookup per
91+ // surviving entry, previously paid only at JFR-flush time (see
92+ // flush_table() below). Only affordable on this, the organic
93+ // GC-driven cleanup path (flush_table()/stop()'s cadence).
94+ jobject ref = env->NewLocalRef (_table[target].ref );
95+ if (ref != nullptr ) {
96+ klass_id = resolveKlassId (env, ref);
97+ if (klass_id != 0 ) {
98+ // Cache the resolution: flush_table() runs its own
99+ // GetObjectClass+Class.getName()+lookupClass() sequence for
100+ // every surviving entry immediately after cleanup_table()
101+ // returns (flush_table() always calls cleanup_table() first),
102+ // which would otherwise repeat this exact JNI round-trip for
103+ // the same object. An object's class is immutable, so this
104+ // value stays valid for flush_table()'s read below, and for
105+ // a later forced sweep's read right below.
106+ _table[target].cached_klass_id = klass_id;
107+ }
108+ env->DeleteLocalRef (ref);
99109 }
100- env->DeleteLocalRef (ref);
110+ } else {
111+ // track()'s table-overflow branch calls cleanup_table(true)
112+ // synchronously from the allocation-sampling call stack (JVMTI
113+ // SampledObjectAlloc callback). resolveKlassId() calls
114+ // Class.getName(), a genuine Java-bytecode upcall (unlike the
115+ // plain native jvmti->GetClassSignature() call
116+ // ObjectSampler::recordAllocation already makes on this same
117+ // callback stack) - too costly, and too re-entrancy-prone via
118+ // the String allocation it can trigger, to run from there. Reuse
119+ // whatever class id an earlier organic epoch already resolved
120+ // for this entry instead; if it was never resolved, this entry's
121+ // sample for this epoch is dropped rather than resolving now.
122+ klass_id = _table[target].cached_klass_id ;
123+ }
124+ if (klass_id != 0 ) {
125+ accumulateKlassCount (klass_id, _table[target].ref );
101126 }
102127 }
103128 } else {
@@ -112,7 +137,7 @@ void LivenessTracker::cleanup_table(bool forced) {
112137
113138 TEST_LOG (" LivenessTracker::cleanup_table survivors=%u klass_count_scratch_size=%d" ,
114139 newsz, _klass_count_scratch_size);
115- if (_gc_generations && !forced && _klass_count_scratch_size > 0 ) {
140+ if (_gc_generations && is_epoch_owner && _klass_count_scratch_size > 0 ) {
116141 foldKlassCountsLocked (env, target_gc_epoch);
117142 }
118143
0 commit comments