| /* |
| * Copyright (c) 2014, 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 "classfile/altHashing.hpp" |
| #include "classfile/javaClasses.inline.hpp" |
| #include "classfile/stringTable.hpp" |
| #include "classfile/vmSymbols.hpp" |
| #include "gc/shared/gc_globals.hpp" |
| #include "gc/shared/oopStorage.hpp" |
| #include "gc/shared/oopStorageSet.hpp" |
| #include "gc/shared/stringdedup/stringDedup.hpp" |
| #include "gc/shared/stringdedup/stringDedupConfig.hpp" |
| #include "gc/shared/stringdedup/stringDedupStat.hpp" |
| #include "gc/shared/stringdedup/stringDedupTable.hpp" |
| #include "logging/log.hpp" |
| #include "logging/logStream.hpp" |
| #include "memory/allocation.hpp" |
| #include "memory/resourceArea.hpp" |
| #include "oops/access.hpp" |
| #include "oops/oopsHierarchy.hpp" |
| #include "oops/typeArrayOop.inline.hpp" |
| #include "oops/weakHandle.inline.hpp" |
| #include "runtime/mutexLocker.hpp" |
| #include "utilities/debug.hpp" |
| #include "utilities/globalDefinitions.hpp" |
| #include "utilities/growableArray.hpp" |
| #include "utilities/macros.hpp" |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // StringDedup::Table::Bucket |
| // |
| // A bucket is a pair of vectors, one containing hash codes, the other |
| // containing values. An "entry" is a corresponding pair of elements from |
| // the vectors. The size of the table is the size of either vector. |
| // |
| // The capacity of the vectors is explicitly controlled, based on the size. |
| // Given N > 0 and 2^N <= size < 2^(N+1), then capacity <= 2^N + k * 2^(N-1) |
| // for the smallest integer k in [0,2] such that size <= capacity. That is, |
| // use a power of 2 or the midpoint between consecutive powers of 2 that is |
| // minimally at least size. When adding an entry and the capacity has been |
| // reached, capacity is increased to the next of those values. |
| // |
| // The main benefit of this representation is that it uses less space than a |
| // more traditional linked-list of entry nodes representation. Such a |
| // representation requires 24 bytes per entry (64 bit platform) for the next |
| // pointer (8 bytes), the value (8 bytes), and the hash code (4 bytes, but |
| // padded to 8 because of alignment requirements). The pair of vectors uses |
| // 12 bytes per entry, but has overhead for excess capacity so that adding |
| // an entry takes amortized constant time. That excess capacity increases |
| // the per entry storage requirement, but it's still better than the linked |
| // list representation. |
| // |
| // The per-bucket cost of a pair of vectors is higher than having a bucket |
| // be the head of a linked list of nodes. We ameliorate this by allowing |
| // buckets to be somewhat longer than is usually desired for a hashtable. |
| // The lookup performance for string deduplication is not that critical, and |
| // searching a vector of hash codes of moderate length should be pretty |
| // fast. By using a good hash function, having different values hash to the |
| // same hash code should be uncommon, making the part of the search of a |
| // bucket for a given hash code more effective. |
| // |
| // The reason to record the hash codes with the values is that comparisons |
| // are expensive, and recomputing the hash code when resizing is also |
| // expensive. A closed hashing implementation with just the values would be |
| // more space efficient. |
| |
| class StringDedup::Table::Bucket { |
| GrowableArrayCHeap<uint, mtStringDedup> _hashes; |
| GrowableArrayCHeap<TableValue, mtStringDedup> _values; |
| |
| void expand_if_full(); |
| |
| public: |
| // precondition: reserve == 0 or is the result of needed_capacity. |
| explicit Bucket(int reserve = 0); |
| |
| ~Bucket() { |
| while (!_values.is_empty()) { |
| _values.pop().release(_table_storage); |
| } |
| } |
| |
| static int needed_capacity(int size); |
| |
| const GrowableArrayView<uint>& hashes() const { return _hashes; } |
| const GrowableArrayView<TableValue>& values() const { return _values; } |
| |
| bool is_empty() const { return _hashes.is_empty(); } |
| int length() const { return _hashes.length(); } |
| |
| void add(uint hash_code, TableValue value) { |
| expand_if_full(); |
| _hashes.push(hash_code); |
| _values.push(value); |
| } |
| |
| void delete_at(int index) { |
| _values.at(index).release(_table_storage); |
| _hashes.delete_at(index); |
| _values.delete_at(index); |
| } |
| |
| void pop_norelease() { |
| _hashes.pop(); |
| _values.pop(); |
| } |
| |
| void shrink(); |
| |
| TableValue find(typeArrayOop obj, uint hash_code) const; |
| |
| void verify(size_t bucket_index, size_t bucket_count) const; |
| }; |
| |
| StringDedup::Table::Bucket::Bucket(int reserve) : |
| _hashes(reserve), _values(reserve) |
| { |
| assert(reserve == needed_capacity(reserve), |
| "reserve %d not computed properly", reserve); |
| } |
| |
| // Choose the least power of 2 or half way between two powers of 2, |
| // such that number of entries <= target. |
| int StringDedup::Table::Bucket::needed_capacity(int needed) { |
| if (needed == 0) return 0; |
| int high = round_up_power_of_2(needed); |
| int low = high - high/4; |
| return (needed <= low) ? low : high; |
| } |
| |
| void StringDedup::Table::Bucket::expand_if_full() { |
| if (_hashes.is_full()) { |
| int needed = needed_capacity(_hashes.capacity() + 1); |
| _hashes.reserve(needed); |
| _values.reserve(needed); |
| } |
| } |
| |
| void StringDedup::Table::Bucket::shrink() { |
| _hashes.shrink_to_fit(); |
| _values.shrink_to_fit(); |
| } |
| |
| StringDedup::Table::TableValue |
| StringDedup::Table::Bucket::find(typeArrayOop obj, uint hash_code) const { |
| int index = 0; |
| for (uint cur_hash : _hashes) { |
| if (cur_hash == hash_code) { |
| typeArrayOop value = cast_from_oop<typeArrayOop>(_values.at(index).peek()); |
| if ((value != nullptr) && |
| java_lang_String::value_equals(obj, value)) { |
| return _values.at(index); |
| } |
| } |
| ++index; |
| } |
| return TableValue(); |
| } |
| |
| void StringDedup::Table::Bucket::verify(size_t bucket_index, |
| size_t bucket_count) const { |
| int entry_count = _hashes.length(); |
| guarantee(entry_count == _values.length(), |
| "hash/value length mismatch: %zu: %d, %d", |
| bucket_index, entry_count, _values.length()); |
| for (uint hash_code : _hashes) { |
| size_t hash_index = hash_code % bucket_count; |
| guarantee(bucket_index == hash_index, |
| "entry in wrong bucket: %zu, %u", bucket_index, hash_code); |
| } |
| size_t index = 0; |
| for (TableValue tv : _values) { |
| guarantee(!tv.is_empty(), "entry missing value: %zu:%zu", bucket_index, index); |
| const oop* p = tv.ptr_raw(); |
| OopStorage::EntryStatus status = _table_storage->allocation_status(p); |
| guarantee(OopStorage::ALLOCATED_ENTRY == status, |
| "bad value: %zu:%zu -> " PTR_FORMAT, bucket_index, index, p2i(p)); |
| // Don't check object is oop_or_null; duplicates OopStorage verify. |
| ++index; |
| } |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // Tracking dead entries |
| // |
| // Keeping track of the number of dead entries in a table is complicated by |
| // the possibility that a GC could be changing the set while we're removing |
| // dead entries. |
| // |
| // If a dead count report is received while cleaning, further cleaning may |
| // reduce the number of dead entries. With STW reference processing one |
| // could maintain an accurate dead count by deducting cleaned entries. But |
| // that doesn't work for concurrent reference processing. In that case the |
| // dead count being reported may include entries that have already been |
| // removed by concurrent cleaning. |
| // |
| // It seems worse to unnecessarily resize or clean than to delay either. So |
| // we track whether the reported dead count is good, and only consider |
| // resizing or cleaning when we have a good idea of the benefit. |
| |
| enum class StringDedup::Table::DeadState { |
| // This is the initial state. This state is also selected when a dead |
| // count report is received and the state is wait1. The reported dead |
| // count is considered good. It might be lower than actual because of an |
| // in-progress concurrent reference processing. It might also increase |
| // immediately due to a new GC. Oh well to both of those. |
| good, |
| // This state is selected when a dead count report is received and the |
| // state is wait2. Current value of dead count may be inaccurate because |
| // of reference processing that was started before or during the most |
| // recent cleaning and finished after. Wait for the next report. |
| wait1, |
| // This state is selected when a cleaning operation completes. Current |
| // value of dead count is inaccurate because we haven't had a report |
| // since the last cleaning. |
| wait2, |
| // Currently cleaning the table. |
| cleaning |
| }; |
| |
| void StringDedup::Table::num_dead_callback(size_t num_dead) { |
| // Lock while modifying dead count and state. |
| MonitorLocker ml(StringDedup_lock, Mutex::_no_safepoint_check_flag); |
| |
| switch (Atomic::load(&_dead_state)) { |
| case DeadState::good: |
| Atomic::store(&_dead_count, num_dead); |
| break; |
| |
| case DeadState::wait1: |
| // Set count first, so dedup thread gets this or a later value if it |
| // sees the good state. |
| Atomic::store(&_dead_count, num_dead); |
| Atomic::release_store(&_dead_state, DeadState::good); |
| break; |
| |
| case DeadState::wait2: |
| Atomic::release_store(&_dead_state, DeadState::wait1); |
| break; |
| |
| case DeadState::cleaning: |
| break; |
| } |
| |
| // Wake up a possibly sleeping dedup thread. This callback is invoked at |
| // the end of a GC, so there may be new requests waiting. |
| ml.notify_all(); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // StringDedup::Table::CleanupState |
| |
| class StringDedup::Table::CleanupState : public CHeapObj<mtStringDedup> { |
| NONCOPYABLE(CleanupState); |
| |
| protected: |
| CleanupState() = default; |
| |
| public: |
| virtual ~CleanupState() = default; |
| virtual bool step() = 0; |
| virtual TableValue find(typeArrayOop obj, uint hash_code) const = 0; |
| virtual void report_end() const = 0; |
| virtual void verify() const = 0; |
| }; |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // StringDedup::Table::Resizer |
| |
| class StringDedup::Table::Resizer final : public CleanupState { |
| Bucket* _buckets; |
| size_t _number_of_buckets; |
| size_t _bucket_index; |
| size_t _shrink_index; |
| |
| public: |
| Resizer(bool grow_only, Bucket* buckets, size_t number_of_buckets) : |
| _buckets(buckets), |
| _number_of_buckets(number_of_buckets), |
| _bucket_index(0), |
| // Disable bucket shrinking if grow_only requested. |
| _shrink_index(grow_only ? Table::_number_of_buckets : 0) |
| { |
| Table::_need_bucket_shrinking = !grow_only; |
| } |
| |
| virtual ~Resizer() { |
| free_buckets(_buckets, _number_of_buckets); |
| } |
| |
| virtual bool step(); |
| |
| virtual TableValue find(typeArrayOop obj, uint hash_code) const { |
| return _buckets[hash_code % _number_of_buckets].find(obj, hash_code); |
| } |
| |
| virtual void report_end() const { |
| _cur_stat.report_resize_table_end(); |
| } |
| |
| virtual void verify() const; |
| }; |
| |
| bool StringDedup::Table::Resizer::step() { |
| if (_bucket_index < _number_of_buckets) { |
| Bucket& bucket = _buckets[_bucket_index]; |
| if (bucket.is_empty()) { |
| bucket.shrink(); // Eagerly release old bucket memory. |
| ++_bucket_index; |
| return true; // Continue transferring with next bucket. |
| } else { |
| uint hash_code = bucket.hashes().last(); |
| TableValue tv = bucket.values().last(); |
| bucket.pop_norelease(); |
| if (tv.peek() != nullptr) { |
| Table::add(tv, hash_code); |
| } else { |
| tv.release(_table_storage); |
| _cur_stat.inc_deleted(); |
| } |
| return true; // Continue transferring current bucket. |
| } |
| } else if (_shrink_index < Table::_number_of_buckets) { |
| // When the new buckets were created, space was reserved based on the |
| // expected number of entries per bucket. But that might be off for any |
| // given bucket. Some will have exceeded that and have been grown as |
| // needed by the insertions. But some might be less and can be shrunk. |
| Table::_buckets[_shrink_index++].shrink(); |
| return true; // Continue shrinking with next bucket. |
| } else { |
| return false; // All buckets transferred and shrunk, so done. |
| } |
| } |
| |
| void StringDedup::Table::Resizer::verify() const { |
| for (size_t i = 0; i < _number_of_buckets; ++i) { |
| _buckets[i].verify(i, _number_of_buckets); |
| } |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // StringDedup::Table::Cleaner |
| |
| class StringDedup::Table::Cleaner final : public CleanupState { |
| size_t _bucket_index; |
| int _entry_index; |
| |
| public: |
| Cleaner() : _bucket_index(0), _entry_index(0) { |
| Table::_need_bucket_shrinking = false; |
| } |
| |
| virtual ~Cleaner() = default; |
| |
| virtual bool step(); |
| |
| virtual TableValue find(typeArrayOop obj, uint hash_code) const { |
| return TableValue(); |
| } |
| |
| virtual void report_end() const { |
| _cur_stat.report_cleanup_table_end(); |
| } |
| |
| virtual void verify() const {} // Nothing to do here. |
| }; |
| |
| bool StringDedup::Table::Cleaner::step() { |
| if (_bucket_index == Table::_number_of_buckets) { |
| return false; // All buckets processed, so done. |
| } |
| Bucket& bucket = Table::_buckets[_bucket_index]; |
| const GrowableArrayView<TableValue>& values = bucket.values(); |
| assert(_entry_index <= values.length(), "invariant"); |
| if (_entry_index == values.length()) { |
| // End of current bucket. Shrink the bucket if oversized for current |
| // usage, and continue at the start of the next bucket. |
| bucket.shrink(); |
| ++_bucket_index; |
| _entry_index = 0; |
| } else if (values.at(_entry_index).peek() == nullptr) { |
| // Current entry is dead. Remove and continue at same index. |
| bucket.delete_at(_entry_index); |
| --Table::_number_of_entries; |
| _cur_stat.inc_deleted(); |
| } else { |
| // Current entry is live. Continue with the next entry. |
| ++_entry_index; |
| } |
| return true; |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // StringDedup::Table |
| |
| OopStorage* StringDedup::Table::_table_storage; |
| StringDedup::Table::Bucket* StringDedup::Table::_buckets; |
| size_t StringDedup::Table::_number_of_buckets; |
| size_t StringDedup::Table::_number_of_entries = 0; |
| size_t StringDedup::Table::_grow_threshold; |
| StringDedup::Table::CleanupState* StringDedup::Table::_cleanup_state = nullptr; |
| bool StringDedup::Table::_need_bucket_shrinking = false; |
| volatile size_t StringDedup::Table::_dead_count = 0; |
| volatile StringDedup::Table::DeadState StringDedup::Table::_dead_state = DeadState::good; |
| |
| void StringDedup::Table::initialize_storage() { |
| assert(_table_storage == nullptr, "storage already created"); |
| _table_storage = OopStorageSet::create_weak("StringDedup Table Weak", mtStringDedup); |
| } |
| |
| void StringDedup::Table::initialize() { |
| size_t num_buckets = Config::initial_table_size(); |
| _buckets = make_buckets(num_buckets); |
| _number_of_buckets = num_buckets; |
| _grow_threshold = Config::grow_threshold(num_buckets); |
| _table_storage->register_num_dead_callback(num_dead_callback); |
| } |
| |
| StringDedup::Table::Bucket* |
| StringDedup::Table::make_buckets(size_t number_of_buckets, size_t reserve) { |
| Bucket* buckets = NEW_C_HEAP_ARRAY(Bucket, number_of_buckets, mtStringDedup); |
| for (size_t i = 0; i < number_of_buckets; ++i) { |
| // Cast because GrowableArray uses int for sizes and such. |
| ::new (&buckets[i]) Bucket(static_cast<int>(reserve)); |
| } |
| return buckets; |
| } |
| |
| void StringDedup::Table::free_buckets(Bucket* buckets, size_t number_of_buckets) { |
| while (number_of_buckets > 0) { |
| buckets[--number_of_buckets].~Bucket(); |
| } |
| FREE_C_HEAP_ARRAY(Bucket, buckets); |
| } |
| |
| // Compute the hash code for obj using halfsiphash_32. As this is a high |
| // quality hash function that is resistant to hashtable flooding, very |
| // unbalanced bucket chains should be rare, and duplicate hash codes within |
| // a bucket should be very rare. |
| uint StringDedup::Table::compute_hash(typeArrayOop obj) { |
| int length = obj->length(); |
| uint64_t hash_seed = Config::hash_seed(); |
| const uint8_t* data = static_cast<uint8_t*>(obj->base(T_BYTE)); |
| return AltHashing::halfsiphash_32(hash_seed, data, length); |
| } |
| |
| size_t StringDedup::Table::hash_to_index(uint hash_code) { |
| return hash_code % _number_of_buckets; |
| } |
| |
| void StringDedup::Table::add(TableValue tv, uint hash_code) { |
| _buckets[hash_to_index(hash_code)].add(hash_code, tv); |
| ++_number_of_entries; |
| } |
| |
| bool StringDedup::Table::is_dead_count_good_acquire() { |
| return Atomic::load_acquire(&_dead_state) == DeadState::good; |
| } |
| |
| // Should be consistent with cleanup_start_if_needed. |
| bool StringDedup::Table::is_grow_needed() { |
| return is_dead_count_good_acquire() && |
| ((_number_of_entries - Atomic::load(&_dead_count)) > _grow_threshold); |
| } |
| |
| // Should be consistent with cleanup_start_if_needed. |
| bool StringDedup::Table::is_dead_entry_removal_needed() { |
| return is_dead_count_good_acquire() && |
| Config::should_cleanup_table(_number_of_entries, Atomic::load(&_dead_count)); |
| } |
| |
| StringDedup::Table::TableValue |
| StringDedup::Table::find(typeArrayOop obj, uint hash_code) { |
| assert(obj != nullptr, "precondition"); |
| if (_cleanup_state != nullptr) { |
| TableValue tv = _cleanup_state->find(obj, hash_code); |
| if (!tv.is_empty()) return tv; |
| } |
| return _buckets[hash_to_index(hash_code)].find(obj, hash_code); |
| } |
| |
| void StringDedup::Table::install(typeArrayOop obj, uint hash_code) { |
| add(TableValue(_table_storage, obj), hash_code); |
| _cur_stat.inc_new(obj->size() * HeapWordSize); |
| } |
| |
| #if INCLUDE_CDS_JAVA_HEAP |
| |
| // Try to look up the string's value array in the shared string table. This |
| // is only worthwhile if sharing is enabled, both at build-time and at |
| // runtime. But it's complicated because we can't trust the is_latin1 value |
| // of the string we're deduplicating. GC requests can provide us with |
| // access to a String that is incompletely constructed; the value could be |
| // set before the coder. |
| bool StringDedup::Table::try_deduplicate_shared(oop java_string) { |
| typeArrayOop value = java_lang_String::value(java_string); |
| assert(value != nullptr, "precondition"); |
| assert(TypeArrayKlass::cast(value->klass())->element_type() == T_BYTE, "precondition"); |
| int length = value->length(); |
| static_assert(sizeof(jchar) == 2 * sizeof(jbyte), "invariant"); |
| assert(((length & 1) == 0) || CompactStrings, "invariant"); |
| if ((length & 1) == 0) { |
| // If the length of the byte array is even, then the value array could be |
| // either non-latin1 or a compact latin1 that happens to have an even length. |
| // For the former case we want to look for a matching shared string. But |
| // for the latter we can still do a lookup, treating the value array as |
| // non-latin1, and deduplicating if we find a match. For deduplication we |
| // only care if the arrays consist of the same sequence of bytes. |
| const jchar* chars = static_cast<jchar*>(value->base(T_CHAR)); |
| oop found = StringTable::lookup_shared(chars, length >> 1); |
| // If found is latin1, then it's byte array differs from the unicode |
| // table key, so not actually a match to value. |
| if ((found != nullptr) && |
| !java_lang_String::is_latin1(found) && |
| try_deduplicate_found_shared(java_string, found)) { |
| return true; |
| } |
| // That didn't work. Try as compact latin1. |
| } |
| // If not using compact strings then don't need to check further. |
| if (!CompactStrings) return false; |
| // Treat value as compact latin1 and try to deduplicate against that. |
| // This works even if java_string is not latin1, but has a byte array with |
| // the same sequence of bytes as a compact latin1 shared string. |
| ResourceMark rm(Thread::current()); |
| jchar* chars = NEW_RESOURCE_ARRAY_RETURN_NULL(jchar, length); |
| if (chars == nullptr) { |
| _cur_stat.inc_skipped_shared(); |
| return true; |
| } |
| for (int i = 0; i < length; ++i) { |
| chars[i] = value->byte_at(i) & 0xff; |
| } |
| oop found = StringTable::lookup_shared(chars, length); |
| if (found == nullptr) return false; |
| assert(java_lang_String::is_latin1(found), "invariant"); |
| return try_deduplicate_found_shared(java_string, found); |
| } |
| |
| bool StringDedup::Table::try_deduplicate_found_shared(oop java_string, oop found) { |
| _cur_stat.inc_known_shared(); |
| typeArrayOop found_value = java_lang_String::value(found); |
| if (found_value == java_lang_String::value(java_string)) { |
| // String's value already matches what's in the table. |
| return true; |
| } else if (deduplicate_if_permitted(java_string, found_value)) { |
| // If java_string has the same coder as found then it won't have |
| // deduplication_forbidden set; interning would have found the matching |
| // shared string. But if they have different coders but happen to have |
| // the same sequence of bytes in their value arrays, then java_string |
| // could have been interned and marked deduplication-forbidden. |
| _cur_stat.inc_deduped(found_value->size() * HeapWordSize); |
| return true; |
| } else { |
| // Must be a mismatch between java_string and found string encodings, |
| // and java_string has been marked deduplication_forbidden, so is |
| // (being) interned in the StringTable. Return false to allow |
| // additional processing that might still lead to some benefit for |
| // deduplication. |
| return false; |
| } |
| } |
| |
| #else // if !INCLUDE_CDS_JAVA_HEAP |
| |
| bool StringDedup::Table::try_deduplicate_shared(oop java_string) { |
| ShouldNotReachHere(); // Call is guarded. |
| return false; |
| } |
| |
| // Undefined because unreferenced. |
| // bool StringDedup::Table::try_deduplicate_found_shared(oop java_string, oop found); |
| |
| #endif // INCLUDE_CDS_JAVA_HEAP |
| |
| bool StringDedup::Table::deduplicate_if_permitted(oop java_string, |
| typeArrayOop value) { |
| // The non-dedup check and value assignment must be under lock. |
| MutexLocker ml(StringDedupIntern_lock, Mutex::_no_safepoint_check_flag); |
| if (java_lang_String::deduplication_forbidden(java_string)) { |
| return false; |
| } else { |
| java_lang_String::set_value(java_string, value); // Dedup! |
| return true; |
| } |
| } |
| |
| void StringDedup::Table::deduplicate(oop java_string) { |
| assert(java_lang_String::is_instance(java_string), "precondition"); |
| _cur_stat.inc_inspected(); |
| if ((StringTable::shared_entry_count() > 0) && |
| try_deduplicate_shared(java_string)) { |
| return; // Done if deduplicated against shared StringTable. |
| } |
| typeArrayOop value = java_lang_String::value(java_string); |
| uint hash_code = compute_hash(value); |
| TableValue tv = find(value, hash_code); |
| if (tv.is_empty()) { |
| // Not in table. Create a new table entry. |
| install(value, hash_code); |
| } else { |
| _cur_stat.inc_known(); |
| typeArrayOop found = cast_from_oop<typeArrayOop>(tv.resolve()); |
| assert(found != nullptr, "invariant"); |
| // Deduplicate if value array differs from what's in the table. |
| if (found != value) { |
| if (deduplicate_if_permitted(java_string, found)) { |
| _cur_stat.inc_deduped(found->size() * HeapWordSize); |
| } else { |
| // If string marked deduplication_forbidden then we can't update its |
| // value. Instead, replace the array in the table with the new one, |
| // as java_string is probably in the StringTable. That makes it a |
| // good target for future deduplications as it is probably intended |
| // to live for some time. |
| tv.replace(value); |
| _cur_stat.inc_replaced(); |
| } |
| } |
| } |
| } |
| |
| bool StringDedup::Table::cleanup_start_if_needed(bool grow_only, bool force) { |
| assert(_cleanup_state == nullptr, "cleanup already in progress"); |
| if (!is_dead_count_good_acquire()) return false; |
| // If dead count is good then we can read it once and use it below |
| // without needing any locking. The recorded count could increase |
| // after the read, but that's okay. |
| size_t dead_count = Atomic::load(&_dead_count); |
| // This assertion depends on dead state tracking. Otherwise, concurrent |
| // reference processing could detect some, but a cleanup operation could |
| // remove them before they are reported. |
| assert(dead_count <= _number_of_entries, "invariant"); |
| size_t adjusted = _number_of_entries - dead_count; |
| if (force || Config::should_grow_table(_number_of_buckets, adjusted)) { |
| return start_resizer(grow_only, adjusted); |
| } else if (grow_only) { |
| return false; |
| } else if (Config::should_shrink_table(_number_of_buckets, adjusted)) { |
| return start_resizer(false /* grow_only */, adjusted); |
| } else if (_need_bucket_shrinking || |
| Config::should_cleanup_table(_number_of_entries, dead_count)) { |
| // Remove dead entries and shrink buckets if needed. |
| return start_cleaner(_number_of_entries, dead_count); |
| } else { |
| // No cleanup needed. |
| return false; |
| } |
| } |
| |
| void StringDedup::Table::set_dead_state_cleaning() { |
| MutexLocker ml(StringDedup_lock, Mutex::_no_safepoint_check_flag); |
| Atomic::store(&_dead_count, size_t(0)); |
| Atomic::store(&_dead_state, DeadState::cleaning); |
| } |
| |
| bool StringDedup::Table::start_resizer(bool grow_only, size_t number_of_entries) { |
| size_t new_size = Config::desired_table_size(number_of_entries); |
| _cur_stat.report_resize_table_start(new_size, _number_of_buckets, number_of_entries); |
| _cleanup_state = new Resizer(grow_only, _buckets, _number_of_buckets); |
| size_t reserve = Bucket::needed_capacity(checked_cast<int>(number_of_entries / new_size)); |
| _buckets = make_buckets(new_size, reserve); |
| _number_of_buckets = new_size; |
| _number_of_entries = 0; |
| _grow_threshold = Config::grow_threshold(new_size); |
| set_dead_state_cleaning(); |
| return true; |
| } |
| |
| bool StringDedup::Table::start_cleaner(size_t number_of_entries, size_t dead_count) { |
| _cur_stat.report_cleanup_table_start(number_of_entries, dead_count); |
| _cleanup_state = new Cleaner(); |
| set_dead_state_cleaning(); |
| return true; |
| } |
| |
| bool StringDedup::Table::cleanup_step() { |
| assert(_cleanup_state != nullptr, "precondition"); |
| return _cleanup_state->step(); |
| } |
| |
| void StringDedup::Table::cleanup_end() { |
| assert(_cleanup_state != nullptr, "precondition"); |
| _cleanup_state->report_end(); |
| delete _cleanup_state; |
| _cleanup_state = nullptr; |
| MutexLocker ml(StringDedup_lock, Mutex::_no_safepoint_check_flag); |
| Atomic::store(&_dead_state, DeadState::wait2); |
| } |
| |
| void StringDedup::Table::verify() { |
| size_t total_count = 0; |
| for (size_t i = 0; i < _number_of_buckets; ++i) { |
| _buckets[i].verify(i, _number_of_buckets); |
| total_count += _buckets[i].length(); |
| } |
| guarantee(total_count == _number_of_entries, |
| "number of values mismatch: %zu counted, %zu recorded", |
| total_count, _number_of_entries); |
| if (_cleanup_state != nullptr) { |
| _cleanup_state->verify(); |
| } |
| } |
| |
| void StringDedup::Table::log_statistics() { |
| size_t dead_count; |
| int dead_state; |
| { |
| MutexLocker ml(StringDedup_lock, Mutex::_no_safepoint_check_flag); |
| dead_count = _dead_count; |
| dead_state = static_cast<int>(_dead_state); |
| } |
| log_debug(stringdedup)("Table: %zu values in %zu buckets, %zu dead (%d)", |
| _number_of_entries, _number_of_buckets, |
| dead_count, dead_state); |
| LogStreamHandle(Trace, stringdedup) log; |
| if (log.is_enabled()) { |
| ResourceMark rm; |
| GrowableArray<size_t> counts; |
| for (size_t i = 0; i < _number_of_buckets; ++i) { |
| int length = _buckets[i].length(); |
| size_t count = counts.at_grow(length); |
| counts.at_put(length, count + 1); |
| } |
| log.print_cr("Table bucket distribution:"); |
| for (int i = 0; i < counts.length(); ++i) { |
| size_t count = counts.at(i); |
| if (count != 0) { |
| log.print_cr(" %4d: %zu", i, count); |
| } |
| } |
| } |
| } |