openjdk/src/hotspot/share/gc/z/zRemembered.cpp
Stefan Karlsson c909216446 8357443: ZGC: Optimize old page iteration in remap remembered phase
Reviewed-by: aboldtch, eosterlund
2025-06-04 14:56:20 +00:00

595 lines
20 KiB
C++

/*
* Copyright (c) 2021, 2025, 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
#include "gc/z/zAddress.inline.hpp"
#include "gc/z/zForwarding.inline.hpp"
#include "gc/z/zGeneration.inline.hpp"
#include "gc/z/zHeap.inline.hpp"
#include "gc/z/zIterator.inline.hpp"
#include "gc/z/zMark.hpp"
#include "gc/z/zPage.inline.hpp"
#include "gc/z/zPageTable.hpp"
#include "gc/z/zRemembered.inline.hpp"
#include "gc/z/zRememberedSet.hpp"
#include "gc/z/zTask.hpp"
#include "gc/z/zVerify.hpp"
#include "memory/iterator.hpp"
#include "oops/oop.inline.hpp"
#include "utilities/bitMap.inline.hpp"
#include "utilities/debug.hpp"
ZRemembered::ZRemembered(ZPageTable* page_table,
const ZForwardingTable* old_forwarding_table,
ZPageAllocator* page_allocator)
: _page_table(page_table),
_old_forwarding_table(old_forwarding_table),
_page_allocator(page_allocator),
_found_old() {}
template <typename Function>
void ZRemembered::oops_do_forwarded_via_containing(GrowableArrayView<ZRememberedSetContaining>* array, Function function) const {
// The array contains duplicated from_addr values. Cache expensive operations.
zaddress_unsafe from_addr = zaddress_unsafe::null;
zaddress to_addr = zaddress::null;
size_t object_size = 0;
for (const ZRememberedSetContaining containing: *array) {
if (from_addr != containing._addr) {
from_addr = containing._addr;
// Relocate object to new location
to_addr = ZGeneration::old()->relocate_or_remap_object(from_addr);
// Figure out size
object_size = ZUtils::object_size(to_addr);
}
// Calculate how far into the from-object the remset entry is
const uintptr_t field_offset = containing._field_addr - from_addr;
// The 'containing' could contain mismatched (addr, addr_field).
// Need to check if the field was within the reported object.
if (field_offset < object_size) {
// Calculate the corresponding address in the to-object
const zaddress to_addr_field = to_addr + field_offset;
function((volatile zpointer*)untype(to_addr_field));
}
}
}
bool ZRemembered::should_scan_page(ZPage* page) const {
if (!ZGeneration::old()->is_phase_relocate()) {
// If the old generation collection is not in the relocation phase, then it
// will not need any synchronization on its forwardings.
return true;
}
ZForwarding* const forwarding = ZGeneration::old()->forwarding(ZOffset::address_unsafe(page->start()));
if (forwarding == nullptr) {
// This page was provably not part of the old relocation set
return true;
}
if (!forwarding->relocated_remembered_fields_is_concurrently_scanned()) {
// Safe to scan
return true;
}
// If we get here, we know that the old collection is concurrently relocating
// objects. We need to be extremely careful not to scan a page that is
// concurrently being in-place relocated because it's objects and previous
// bits could be concurrently be moving around.
//
// Before calling this function ZRemembered::scan_forwarding ensures
// that all forwardings that have not already been fully relocated,
// will have had their "previous" remembered set bits scanned.
//
// The current page we're currently scanning could either be the same page
// that was found during scan_forwarding, or it could have been replaced
// by a new "allocating" page. There are two situations we have to consider:
//
// 1) If it is a proper new allocating page, then all objects where copied
// after scan_forwarding ran, and we are guaranteed that no "previous"
// remembered set bits are set. So, there's no need to scan this page.
//
// 2) If this is an in-place relocated page, then the entire page could
// be concurrently relocated. Meaning that both objects and previous
// remembered set bits could be moving around. However, if the in-place
// relocation is ongoing, we've already scanned all relevant "previous"
// bits when calling scan_forwarding. So, this page *must* not be scanned.
//
// Don't scan the page.
return false;
}
bool ZRemembered::scan_page_and_clear_remset(ZPage* page) const {
const bool can_trust_live_bits =
page->is_relocatable() && !ZGeneration::old()->is_phase_mark();
bool result = false;
if (!can_trust_live_bits) {
// We don't have full liveness info - scan all remset entries
page->log_msg(" (scan_page_remembered)");
int count = 0;
page->oops_do_remembered([&](volatile zpointer* p) {
result |= scan_field(p);
count++;
});
page->log_msg(" (scan_page_remembered done: %d ignoring: " PTR_FORMAT " )", count, p2i(page->remset_current()));
} else if (page->is_marked()) {
// We have full liveness info - Only scan remset entries in live objects
page->log_msg(" (scan_page_remembered_in_live)");
page->oops_do_remembered_in_live([&](volatile zpointer* p) {
result |= scan_field(p);
});
} else {
page->log_msg(" (scan_page_remembered_dead)");
// All objects are dead - do nothing
}
if (ZVerifyRemembered) {
// Make sure self healing of pointers is ordered before clearing of
// the previous bits so that ZVerify::after_scan can detect missing
// remset entries accurately.
OrderAccess::storestore();
}
// If we have consumed the remset entries above we also clear them.
// The exception is if the page is completely empty/garbage, where we don't
// want to race with an old collection modifying the remset as well.
if (!can_trust_live_bits || page->is_marked()) {
page->clear_remset_previous();
}
return result;
}
static void fill_containing(GrowableArrayCHeap<ZRememberedSetContaining, mtGC>* array, ZPage* page) {
page->log_msg(" (fill_remembered_containing)");
ZRememberedSetContainingIterator iter(page);
for (ZRememberedSetContaining containing; iter.next(&containing);) {
array->push(containing);
}
}
struct ZRememberedScanForwardingContext {
GrowableArrayCHeap<ZRememberedSetContaining, mtGC> _containing_array;
struct Where {
static const int NumRecords = 10;
Tickspan _duration;
int _count;
Tickspan _max_durations[NumRecords];
int _max_count;
Where()
: _duration(),
_count(),
_max_durations(),
_max_count() {}
void report(const Tickspan& duration) {
_duration += duration;
_count++;
// Install into max array
for (int i = 0; i < NumRecords; i++) {
if (duration > _max_durations[i]) {
// Slid to the side
for (int j = _max_count - 1; i < j; j--) {
_max_durations[j] = _max_durations[j - 1];
}
// Install
_max_durations[i] = duration;
if (_max_count < NumRecords) {
_max_count++;
}
break;
}
}
}
void print(const char* name) {
log_debug(gc, remset)("Remset forwarding %s: %.3fms count: %d %s",
name, TimeHelper::counter_to_millis(_duration.value()), _count, Thread::current()->name());
for (int i = 0; i < _max_count; i++) {
log_debug(gc, remset)(" %.3fms", TimeHelper::counter_to_millis(_max_durations[i].value()));
}
}
};
Where _where[2];
ZRememberedScanForwardingContext()
: _containing_array(),
_where() {}
~ZRememberedScanForwardingContext() {
print();
}
void report_retained(const Tickspan& duration) {
_where[0].report(duration);
}
void report_released(const Tickspan& duration) {
_where[1].report(duration);
}
void print() {
_where[0].print("retained");
_where[1].print("released");
}
};
struct ZRememberedScanForwardingMeasureRetained {
ZRememberedScanForwardingContext* _context;
Ticks _start;
ZRememberedScanForwardingMeasureRetained(ZRememberedScanForwardingContext* context)
: _context(context),
_start(Ticks::now()) {
}
~ZRememberedScanForwardingMeasureRetained() {
const Ticks end = Ticks::now();
const Tickspan duration = end - _start;
_context->report_retained(duration);
}
};
struct ZRememberedScanForwardingMeasureReleased {
ZRememberedScanForwardingContext* _context;
Ticks _start;
ZRememberedScanForwardingMeasureReleased(ZRememberedScanForwardingContext* context)
: _context(context),
_start(Ticks::now()) {
}
~ZRememberedScanForwardingMeasureReleased() {
const Ticks end = Ticks::now();
const Tickspan duration = end - _start;
_context->report_released(duration);
}
};
bool ZRemembered::scan_forwarding(ZForwarding* forwarding, void* context_void) const {
ZRememberedScanForwardingContext* const context = (ZRememberedScanForwardingContext*)context_void;
bool result = false;
if (forwarding->retain_page(ZGeneration::old()->relocate_queue())) {
ZRememberedScanForwardingMeasureRetained measure(context);
forwarding->page()->log_msg(" (scan_forwarding)");
// We don't want to wait for the old relocation to finish and publish all
// relocated remembered fields. Reject its fields and collect enough data
// up-front.
forwarding->relocated_remembered_fields_notify_concurrent_scan_of();
// Collect all remset info while the page is retained
GrowableArrayCHeap<ZRememberedSetContaining, mtGC>* array = &context->_containing_array;
array->clear();
fill_containing(array, forwarding->page());
forwarding->release_page();
// Relocate (and mark) while page is released, to prevent
// retain deadlock when relocation threads in-place relocate.
oops_do_forwarded_via_containing(array, [&](volatile zpointer* p) {
result |= scan_field(p);
});
} else {
ZRememberedScanForwardingMeasureReleased measure(context);
// The page has been released. If the page was relocated while this young
// generation collection was running, the old generation relocation will
// have published all addresses of fields that had a remembered set entry.
forwarding->relocated_remembered_fields_apply_to_published([&](volatile zpointer* p) {
result |= scan_field(p);
});
}
return result;
}
// When scanning the remembered set during the young generation marking, we
// want to visit all old pages. And we want that to be done in parallel and
// fast.
//
// Walking over the entire page table and letting the workers claim indices
// have been shown to have scalability issues.
//
// So, we have the "found old" optimization, which allows us to perform much
// fewer claims (order of old pages, instead of order of slots in the page
// table), and it allows us to read fewer pages.
//
// The set of "found old pages" isn't precise, and can contain stale entries
// referring to slots of freed pages, or even slots where young pages have
// been installed. However, it will not lack any of the old pages.
//
// The data is maintained very similar to when and how we maintain the
// remembered set bits: We keep two separates sets, one for read-only access
// by the young marking, and a currently active set where we register new
// pages. When pages get relocated, or die, the page table slot for that page
// must be cleared. This clearing is done just like we do with the remset
// scanning: The old entries are not copied to the current active set, only
// slots that were found to actually contain old pages are registered in the
// active set.
ZRemembered::FoundOld::FoundOld()
// Array initialization requires copy constructors, which CHeapBitMap
// doesn't provide. Instantiate two instances, and populate an array
// with pointers to the two instances.
: _allocated_bitmap_0{ZAddressOffsetMax >> ZGranuleSizeShift, mtGC, true /* clear */},
_allocated_bitmap_1{ZAddressOffsetMax >> ZGranuleSizeShift, mtGC, true /* clear */},
_bitmaps{&_allocated_bitmap_0, &_allocated_bitmap_1},
_current{0} {}
BitMap* ZRemembered::FoundOld::current_bitmap() {
return _bitmaps[_current];
}
BitMap* ZRemembered::FoundOld::previous_bitmap() {
return _bitmaps[_current ^ 1];
}
void ZRemembered::FoundOld::flip() {
_current ^= 1;
}
void ZRemembered::FoundOld::clear_previous() {
previous_bitmap()->clear_large();
}
void ZRemembered::FoundOld::register_page(ZPage* page) {
assert(page->is_old(), "Only register old pages");
current_bitmap()->par_set_bit(untype(page->start()) >> ZGranuleSizeShift, memory_order_relaxed);
}
void ZRemembered::flip_found_old_sets() {
_found_old.flip();
}
void ZRemembered::clear_found_old_previous_set() {
_found_old.clear_previous();
}
void ZRemembered::register_found_old(ZPage* page) {
assert(page->is_old(), "Should only register old pages");
_found_old.register_page(page);
}
struct ZRemsetTableEntry {
ZPage* _page;
ZForwarding* _forwarding;
};
ZRemsetTableIterator::ZRemsetTableIterator(ZRemembered* remembered, bool previous)
: _remembered(remembered),
_bm(previous
? _remembered->_found_old.previous_bitmap()
: _remembered->_found_old.current_bitmap()),
_page_table(remembered->_page_table),
_old_forwarding_table(remembered->_old_forwarding_table),
_claimed(0) {}
// This iterator uses the "found old" optimization.
bool ZRemsetTableIterator::next(ZRemsetTableEntry* entry_addr) {
BitMap::idx_t prev = Atomic::load(&_claimed);
for (;;) {
if (prev == _bm->size()) {
return false;
}
const BitMap::idx_t page_index = _bm->find_first_set_bit(_claimed);
if (page_index == _bm->size()) {
Atomic::cmpxchg(&_claimed, prev, page_index, memory_order_relaxed);
return false;
}
const BitMap::idx_t res = Atomic::cmpxchg(&_claimed, prev, page_index + 1, memory_order_relaxed);
if (res != prev) {
// Someone else claimed
prev = res;
continue;
}
// Found bit - look around for page or forwarding to scan
ZForwarding* forwarding = nullptr;
if (ZGeneration::old()->is_phase_relocate()) {
forwarding = _old_forwarding_table->at(page_index);
}
ZPage* page = _page_table->at(page_index);
if (page != nullptr && !page->is_old()) {
page = nullptr;
}
if (page == nullptr && forwarding == nullptr) {
// Nothing to scan
continue;
}
// Found old page or old forwarding
entry_addr->_forwarding = forwarding;
entry_addr->_page = page;
return true;
}
}
void ZRemembered::remap_current(ZRemsetTableIterator* iter) {
for (ZRemsetTableEntry entry; iter->next(&entry);) {
assert(entry._forwarding == nullptr, "Shouldn't be looking for forwardings");
assert(entry._page != nullptr, "Must have found a page");
assert(entry._page->is_old(), "Should only have found old pages");
entry._page->oops_do_current_remembered(ZBarrier::load_barrier_on_oop_field);
}
}
// This task scans the remembered set and follows pointers when possible.
// Interleaving remembered set scanning with marking makes the marking times
// lower and more predictable.
class ZRememberedScanMarkFollowTask : public ZRestartableTask {
private:
ZRemembered* const _remembered;
ZMark* const _mark;
ZRemsetTableIterator _remset_table_iterator;
public:
ZRememberedScanMarkFollowTask(ZRemembered* remembered, ZMark* mark)
: ZRestartableTask("ZRememberedScanMarkFollowTask"),
_remembered(remembered),
_mark(mark),
_remset_table_iterator(remembered, true /* previous */) {
_mark->prepare_work();
_remembered->_page_allocator->enable_safe_destroy();
}
~ZRememberedScanMarkFollowTask() {
_remembered->_page_allocator->disable_safe_destroy();
_mark->finish_work();
// We are done scanning the set of old pages.
// Clear the set for the next young collection.
_remembered->clear_found_old_previous_set();
}
virtual void work_inner() {
ZRememberedScanForwardingContext context;
// Follow initial roots
if (!_mark->follow_work_partial()) {
// Bail
return;
}
for (ZRemsetTableEntry entry; _remset_table_iterator.next(&entry);) {
bool left_marking = false;
ZForwarding* forwarding = entry._forwarding;
ZPage* page = entry._page;
// Scan forwarding
if (forwarding != nullptr) {
bool found_roots = _remembered->scan_forwarding(forwarding, &context);
ZVerify::after_scan(forwarding);
if (found_roots) {
// Follow remembered set when possible
left_marking = !_mark->follow_work_partial();
}
}
// Scan page
if (page != nullptr) {
if (_remembered->should_scan_page(page)) {
// Visit all entries pointing into young gen
bool found_roots = _remembered->scan_page_and_clear_remset(page);
if (found_roots && !left_marking) {
// Follow remembered set when possible
left_marking = !_mark->follow_work_partial();
}
}
// The remset scanning maintains the "maybe old" pages optimization.
//
// We maintain two sets of old pages: The first is the currently active
// set, where old pages are registered into. The second is the old
// read-only copy. The two sets flip during young mark start. This
// analogous to how we set and clean remembered set bits.
//
// The iterator reads from the read-only copy, and then here, we install
// entries in the current active set.
_remembered->register_found_old(page);
}
SuspendibleThreadSet::yield();
if (left_marking) {
// Bail
return;
}
}
_mark->follow_work_complete();
}
virtual void work() {
SuspendibleThreadSetJoiner sts_joiner;
work_inner();
// We might have found pointers into the other generation, and then we want to
// publish such marking stacks to prevent that generation from getting a mark continue.
// We also flush in case of a resize where a new worker thread continues the marking
// work, causing a mark continue for the collected generation.
ZHeap::heap()->mark_flush(Thread::current());
}
virtual void resize_workers(uint nworkers) {
_mark->resize_workers(nworkers);
}
};
void ZRemembered::scan_and_follow(ZMark* mark) {
{
// Follow the object graph and lazily scan the remembered set
ZRememberedScanMarkFollowTask task(this, mark);
ZGeneration::young()->workers()->run(&task);
// Try to terminate after following the graph
if (ZAbort::should_abort() || !mark->try_terminate_flush()) {
return;
}
}
// If flushing failed, we have to restart marking again, but this time we don't need to
// scan the remembered set.
mark->mark_follow();
}
bool ZRemembered::scan_field(volatile zpointer* p) const {
assert(ZGeneration::young()->is_phase_mark(), "Wrong phase");
const zaddress addr = ZBarrier::remset_barrier_on_oop_field(p);
if (!is_null(addr) && ZHeap::heap()->is_young(addr)) {
remember(p);
return true;
}
return false;
}
void ZRemembered::flip() {
ZRememberedSet::flip();
flip_found_old_sets();
}