--- old/make/excludeSrc.make 2014-03-07 14:31:26.897538090 +0100 +++ new/make/excludeSrc.make 2014-03-07 14:31:26.776965464 +0100 @@ -89,7 +89,7 @@ g1MMUTracker.cpp g1MarkSweep.cpp g1MemoryPool.cpp g1MonitoringSupport.cpp g1OopClosures.cpp \ g1RemSet.cpp g1RemSetSummary.cpp g1SATBCardTableModRefBS.cpp g1_globals.cpp heapRegion.cpp \ g1BiasedArray.cpp heapRegionRemSet.cpp heapRegionSeq.cpp heapRegionSet.cpp heapRegionSets.cpp \ - ptrQueue.cpp satbQueue.cpp sparsePRT.cpp survRateGroup.cpp vm_operations_g1.cpp \ + ptrQueue.cpp satbQueue.cpp sparsePRT.cpp stringDedup.cpp survRateGroup.cpp vm_operations_g1.cpp \ adjoiningGenerations.cpp adjoiningVirtualSpaces.cpp asPSOldGen.cpp asPSYoungGen.cpp \ cardTableExtension.cpp gcTaskManager.cpp gcTaskThread.cpp objectStartArray.cpp \ parallelScavengeHeap.cpp parMarkBitMap.cpp pcTasks.cpp psAdaptiveSizePolicy.cpp \ --- old/src/share/vm/classfile/javaClasses.hpp 2014-03-07 14:31:27.577542256 +0100 +++ new/src/share/vm/classfile/javaClasses.hpp 2014-03-07 14:31:27.457578272 +0100 @@ -61,10 +61,6 @@ static Handle basic_create(int length, TRAPS); - static void set_value( oop string, typeArrayOop buffer) { - assert(initialized, "Must be initialized"); - string->obj_field_put(value_offset, (oop)buffer); - } static void set_offset(oop string, int offset) { assert(initialized, "Must be initialized"); if (offset_offset > 0) { @@ -122,12 +118,26 @@ return hash_offset; } + static void set_value(oop string, typeArrayOop buffer) { + assert(initialized && (value_offset > 0), "Must be initialized"); + string->obj_field_put(value_offset, (oop)buffer); + } + static void set_hash(oop string, unsigned int hash) { + assert(initialized && (hash_offset > 0), "Must be initialized"); + string->int_field_put(hash_offset, hash); + } + // Accessors static typeArrayOop value(oop java_string) { assert(initialized && (value_offset > 0), "Must be initialized"); assert(is_instance(java_string), "must be java_string"); return (typeArrayOop) java_string->obj_field(value_offset); } + static unsigned int hash(oop java_string) { + assert(initialized && (hash_offset > 0), "Must be initialized"); + assert(is_instance(java_string), "must be java_string"); + return java_string->int_field(hash_offset); + } static int offset(oop java_string) { assert(initialized, "Must be initialized"); assert(is_instance(java_string), "must be java_string"); --- old/src/share/vm/classfile/symbolTable.cpp 2014-03-07 14:31:28.277542539 +0100 +++ new/src/share/vm/classfile/symbolTable.cpp 2014-03-07 14:31:28.157566069 +0100 @@ -35,6 +35,9 @@ #include "oops/oop.inline2.hpp" #include "runtime/mutexLocker.hpp" #include "utilities/hashtable.inline.hpp" +#if INCLUDE_ALL_GCS +#include "gc_implementation/g1/stringDedup.hpp" +#endif // -------------------------------------------------------------------------- @@ -728,6 +731,15 @@ string = java_lang_String::create_from_unicode(name, len, CHECK_NULL); } +#if INCLUDE_ALL_GCS + if (UseG1GC && UseStringDeduplication) { + // Deduplicate the string before it is interned. Note that we should never + // deduplicate a string after it has been interned. Doing so will counteract + // compiler optimizations done on e.g. interned string literals. + StringDedup::deduplicate(string()); + } +#endif + // Grab the StringTable_lock before getting the_table() because it could // change at safepoint. MutexLocker ml(StringTable_lock, THREAD); --- old/src/share/vm/gc_implementation/g1/concurrentMark.cpp 2014-03-07 14:31:28.986446987 +0100 +++ new/src/share/vm/gc_implementation/g1/concurrentMark.cpp 2014-03-07 14:31:28.856446127 +0100 @@ -35,6 +35,7 @@ #include "gc_implementation/g1/heapRegion.inline.hpp" #include "gc_implementation/g1/heapRegionRemSet.hpp" #include "gc_implementation/g1/heapRegionSeq.inline.hpp" +#include "gc_implementation/g1/stringDedup.hpp" #include "gc_implementation/shared/vmGCOperations.hpp" #include "gc_implementation/shared/gcTimer.hpp" #include "gc_implementation/shared/gcTrace.hpp" @@ -2533,6 +2534,9 @@ return; } + // Unlink stale oops in string deduplication queue/table + StringDedup::unlink(&g1_is_alive); + g1h->unlink_string_and_symbol_table(&g1_is_alive, /* process_strings */ false, // currently strings are always roots /* process_symbols */ true); --- old/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp 2014-03-07 14:31:29.827542221 +0100 +++ new/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp 2014-03-07 14:31:29.686562602 +0100 @@ -43,6 +43,7 @@ #include "gc_implementation/g1/heapRegion.inline.hpp" #include "gc_implementation/g1/heapRegionRemSet.hpp" #include "gc_implementation/g1/heapRegionSeq.inline.hpp" +#include "gc_implementation/g1/stringDedup.hpp" #include "gc_implementation/g1/vm_operations_g1.hpp" #include "gc_implementation/shared/gcHeapSummary.hpp" #include "gc_implementation/shared/gcTimer.hpp" @@ -2182,6 +2183,8 @@ // values in the heap have been properly initialized. _g1mm = new G1MonitoringSupport(this); + StringDedup::initialize(); + return JNI_OK; } @@ -3462,6 +3465,11 @@ if (!silent) gclog_or_tty->print("RemSet "); rem_set()->verify(); + if (UseStringDeduplication) { + if (!silent) gclog_or_tty->print("StrDedup "); + StringDedup::verify(); + } + if (failures) { gclog_or_tty->print_cr("Heap:"); // It helps to have the per-region information in the output to @@ -3479,8 +3487,13 @@ } guarantee(!failures, "there should not have been any failures"); } else { - if (!silent) - gclog_or_tty->print("(SKIPPING roots, heapRegionSets, heapRegions, remset) "); + if (!silent) { + gclog_or_tty->print("(SKIPPING Roots, HeapRegionSets, HeapRegions, RemSet"); + if (UseStringDeduplication) { + gclog_or_tty->print(", StrDedup"); + } + gclog_or_tty->print(") "); + } } } @@ -3573,6 +3586,7 @@ st->cr(); _cm->print_worker_threads_on(st); _cg1r->print_worker_threads_on(st); + StringDedup::print_worker_threads_on(st); } void G1CollectedHeap::gc_threads_do(ThreadClosure* tc) const { @@ -3581,6 +3595,7 @@ } tc->do_thread(_cmThread); _cg1r->threads_do(tc); + StringDedup::threads_do(tc); } void G1CollectedHeap::print_tracing_info() const { @@ -4760,6 +4775,13 @@ obj->set_mark(m); } + if (UseStringDeduplication) { + StringDedup::enqueue_from_evacuation(from_region->is_young(), + to_region->is_young(), + queue_num(), + obj); + } + size_t* surv_young_words = surviving_young_words(); surv_young_words[young_index] += word_sz; @@ -5889,6 +5911,7 @@ G1STWIsAliveClosure is_alive(this); G1KeepAliveClosure keep_alive(this); JNIHandles::weak_oops_do(&is_alive, &keep_alive); + StringDedup::unlink_or_oops_do(&is_alive, &keep_alive); } release_gc_alloc_regions(n_workers, evacuation_info); @@ -6408,9 +6431,10 @@ TearDownRegionSetsClosure cl(&_old_set); heap_region_iterate(&cl); - // Need to do this after the heap iteration to be able to - // recognize the young regions and ignore them during the iteration. - _young_list->empty_list(); + // Note that emptying the _young_list is postponed and instead done as + // the first step when rebuilding the regions sets again. The reason for + // this is that during a full GC string deduplication needs to know if + // a collected region was young or old when the full GC was initiated. } _free_list.remove_all(); } @@ -6464,6 +6488,10 @@ void G1CollectedHeap::rebuild_region_sets(bool free_list_only) { assert_at_safepoint(true /* should_be_vm_thread */); + if (!free_list_only) { + _young_list->empty_list(); + } + RebuildRegionSetsClosure cl(free_list_only, &_old_set, &_free_list); heap_region_iterate(&cl); --- old/src/share/vm/gc_implementation/g1/g1GCPhaseTimes.cpp 2014-03-07 14:31:30.706446199 +0100 +++ new/src/share/vm/gc_implementation/g1/g1GCPhaseTimes.cpp 2014-03-07 14:31:30.566600399 +0100 @@ -168,7 +168,9 @@ _last_termination_attempts(_max_gc_threads, SIZE_FORMAT), _last_gc_worker_end_times_ms(_max_gc_threads, "%.1lf", false), _last_gc_worker_times_ms(_max_gc_threads, "%.1lf"), - _last_gc_worker_other_times_ms(_max_gc_threads, "%.1lf") + _last_gc_worker_other_times_ms(_max_gc_threads, "%.1lf"), + _cur_string_dedup_queue_fixup_worker_times_ms(_max_gc_threads, "%.1lf"), + _cur_string_dedup_table_fixup_worker_times_ms(_max_gc_threads, "%.1lf") { assert(max_gc_threads > 0, "Must have some GC threads"); } @@ -192,6 +194,9 @@ _last_gc_worker_end_times_ms.reset(); _last_gc_worker_times_ms.reset(); _last_gc_worker_other_times_ms.reset(); + + _cur_string_dedup_queue_fixup_worker_times_ms.reset(); + _cur_string_dedup_table_fixup_worker_times_ms.reset(); } void G1GCPhaseTimes::note_gc_end() { @@ -227,6 +232,19 @@ _last_gc_worker_times_ms.verify(); _last_gc_worker_other_times_ms.verify(); + + _cur_string_dedup_queue_fixup_worker_times_ms.verify(); + _cur_string_dedup_table_fixup_worker_times_ms.verify(); +} + +void G1GCPhaseTimes::note_string_dedup_fixup_start() { + _cur_string_dedup_queue_fixup_worker_times_ms.reset(); + _cur_string_dedup_table_fixup_worker_times_ms.reset(); +} + +void G1GCPhaseTimes::note_string_dedup_fixup_end() { + _cur_string_dedup_queue_fixup_worker_times_ms.verify(); + _cur_string_dedup_table_fixup_worker_times_ms.verify(); } void G1GCPhaseTimes::print_stats(int level, const char* str, double value) { @@ -250,6 +268,9 @@ // Strong code root migration time misc_time_ms += _cur_strong_code_root_migration_time_ms; + // Now subtract the time taken to fix up weak roots + misc_time_ms += _cur_string_dedup_fixup_time_ms; + // Subtract the time taken to clean the card table from the // current value of "other time" misc_time_ms += _cur_clear_ct_time_ms; @@ -299,6 +320,11 @@ } print_stats(1, "Code Root Fixup", _cur_collection_code_root_fixup_time_ms); print_stats(1, "Code Root Migration", _cur_strong_code_root_migration_time_ms); + if (UseStringDeduplication) { + print_stats(1, "String Dedup Fixup", _cur_string_dedup_fixup_time_ms, _active_gc_threads); + _cur_string_dedup_queue_fixup_worker_times_ms.print(2, "Queue Fixup (ms)"); + _cur_string_dedup_table_fixup_worker_times_ms.print(2, "Table Fixup (ms)"); + } print_stats(1, "Clear CT", _cur_clear_ct_time_ms); double misc_time_ms = pause_time_sec * MILLIUNITS - accounted_time_ms(); print_stats(1, "Other", misc_time_ms); --- old/src/share/vm/gc_implementation/g1/g1GCPhaseTimes.hpp 2014-03-07 14:31:31.436351531 +0100 +++ new/src/share/vm/gc_implementation/g1/g1GCPhaseTimes.hpp 2014-03-07 14:31:31.317566207 +0100 @@ -132,6 +132,10 @@ double _cur_collection_code_root_fixup_time_ms; double _cur_strong_code_root_migration_time_ms; + double _cur_string_dedup_fixup_time_ms; + WorkerDataArray _cur_string_dedup_queue_fixup_worker_times_ms; + WorkerDataArray _cur_string_dedup_table_fixup_worker_times_ms; + double _cur_clear_ct_time_ms; double _cur_ref_proc_time_ms; double _cur_ref_enq_time_ms; @@ -223,6 +227,21 @@ _cur_strong_code_root_migration_time_ms = ms; } + void note_string_dedup_fixup_start(); + void note_string_dedup_fixup_end(); + + void record_string_dedup_fixup_time(double ms) { + _cur_string_dedup_fixup_time_ms = ms; + } + + void record_string_dedup_queue_fixup_worker_time(uint worker_id, double ms) { + _cur_string_dedup_queue_fixup_worker_times_ms.set(worker_id, ms); + } + + void record_string_dedup_table_fixup_worker_time(uint worker_id, double ms) { + _cur_string_dedup_table_fixup_worker_times_ms.set(worker_id, ms); + } + void record_ref_proc_time(double ms) { _cur_ref_proc_time_ms = ms; } --- old/src/share/vm/gc_implementation/g1/g1MarkSweep.cpp 2014-03-07 14:31:32.177546549 +0100 +++ new/src/share/vm/gc_implementation/g1/g1MarkSweep.cpp 2014-03-07 14:31:32.027590248 +0100 @@ -31,6 +31,7 @@ #include "code/icBuffer.hpp" #include "gc_implementation/g1/g1Log.hpp" #include "gc_implementation/g1/g1MarkSweep.hpp" +#include "gc_implementation/g1/stringDedup.hpp" #include "gc_implementation/shared/gcHeapSummary.hpp" #include "gc_implementation/shared/gcTimer.hpp" #include "gc_implementation/shared/gcTrace.hpp" @@ -161,6 +162,9 @@ // Prune dead klasses from subklass/sibling/implementor lists. Klass::clean_weak_klass_links(&GenMarkSweep::is_alive); + // Delete dead entries in string deduplication queue/table + StringDedup::unlink(&GenMarkSweep::is_alive); + // Delete entries for dead interned string and clean up unreferenced symbols in symbol table. G1CollectedHeap::heap()->unlink_string_and_symbol_table(&GenMarkSweep::is_alive); @@ -317,6 +321,9 @@ // have been cleared if they pointed to non-surviving objects.) sh->process_weak_roots(&GenMarkSweep::adjust_pointer_closure); + // Adjust pointers in the string deduplication queue/table + StringDedup::oops_do(&GenMarkSweep::adjust_pointer_closure); + GenMarkSweep::adjust_marks(); G1AdjustPointersClosure blk; --- old/src/share/vm/gc_implementation/shared/markSweep.inline.hpp 2014-03-07 14:31:32.877552666 +0100 +++ new/src/share/vm/gc_implementation/shared/markSweep.inline.hpp 2014-03-07 14:31:32.757574459 +0100 @@ -30,10 +30,17 @@ #include "utilities/stack.inline.hpp" #include "utilities/macros.hpp" #if INCLUDE_ALL_GCS +#include "gc_implementation/g1/stringDedup.hpp" #include "gc_implementation/parallelScavenge/psParallelCompact.hpp" #endif // INCLUDE_ALL_GCS inline void MarkSweep::mark_object(oop obj) { +#if INCLUDE_ALL_GCS + if (UseG1GC && UseStringDeduplication) { + // We must enqueue the object before it is marked + StringDedup::enqueue_from_mark(obj); + } +#endif // some marks may contain information we need to preserve so we store them away // and overwrite the mark. We'll restore it at the end of markSweep. markOop mark = obj->mark(); --- old/src/share/vm/runtime/arguments.cpp 2014-03-07 14:31:33.597558747 +0100 +++ new/src/share/vm/runtime/arguments.cpp 2014-03-07 14:31:33.457588332 +0100 @@ -2245,6 +2245,10 @@ "G1ConcRSHotCardLimit"); status = status && verify_interval(G1ConcRSLogCacheSize, 0, 31, "G1ConcRSLogCacheSize"); + if (UseStringDeduplication) { + status = status && verify_interval(StringDeduplicationAgeThreshold, 1, markOopDesc::max_age, + "StringDeduplicationAgeThreshold"); + } } if (UseConcMarkSweepGC) { status = status && verify_min_value(CMSOldPLABNumRefills, 1, "CMSOldPLABNumRefills"); --- old/src/share/vm/runtime/globals.hpp 2014-03-07 14:31:34.387546522 +0100 +++ new/src/share/vm/runtime/globals.hpp 2014-03-07 14:31:34.246432421 +0100 @@ -3836,6 +3836,21 @@ experimental(uintx, SymbolTableSize, defaultSymbolTableSize, \ "Number of buckets in the JVM internal Symbol table") \ \ + product(bool, UseStringDeduplication, false, \ + "Use string deduplication") \ + \ + product(bool, PrintStringDeduplicationStatistics, false, \ + "Print string deduplication statistics") \ + \ + product(uintx, StringDeduplicationAgeThreshold, 3, \ + "The age a string must reach to be considered for deduplication") \ + \ + diagnostic(bool, StringDeduplicationResizeALot, false, \ + "Force table resize every time the table is scanned") \ + \ + diagnostic(bool, StringDeduplicationRehashALot, false, \ + "Force table rehash every time the table is scanned") \ + \ develop(bool, TraceDefaultMethods, false, \ "Trace the default method processing steps") \ \ --- old/src/share/vm/runtime/mutexLocker.cpp 2014-03-07 14:31:35.157563452 +0100 +++ new/src/share/vm/runtime/mutexLocker.cpp 2014-03-07 14:31:35.027380411 +0100 @@ -58,6 +58,8 @@ Mutex* VtableStubs_lock = NULL; Mutex* SymbolTable_lock = NULL; Mutex* StringTable_lock = NULL; +Monitor* StringDedupQueue_lock = NULL; +Mutex* StringDedupTable_lock = NULL; Mutex* CodeCache_lock = NULL; Mutex* MethodData_lock = NULL; Mutex* RetData_lock = NULL; @@ -196,6 +198,9 @@ def(MMUTracker_lock , Mutex , leaf , true ); def(HotCardCache_lock , Mutex , special , true ); def(EvacFailureStack_lock , Mutex , nonleaf , true ); + + def(StringDedupQueue_lock , Monitor, leaf, true ); + def(StringDedupTable_lock , Mutex , leaf, true ); } def(ParGCRareEvent_lock , Mutex , leaf , true ); def(DerivedPointerTableGC_lock , Mutex, leaf, true ); --- old/src/share/vm/runtime/mutexLocker.hpp 2014-03-07 14:31:35.857554714 +0100 +++ new/src/share/vm/runtime/mutexLocker.hpp 2014-03-07 14:31:35.727539459 +0100 @@ -66,6 +66,8 @@ extern Mutex* VtableStubs_lock; // a lock on the VtableStubs extern Mutex* SymbolTable_lock; // a lock on the symbol table extern Mutex* StringTable_lock; // a lock on the interned string table +extern Monitor* StringDedupQueue_lock; // a lock on the string deduplication queue +extern Mutex* StringDedupTable_lock; // a lock on the string deduplication table extern Mutex* CodeCache_lock; // a lock on the CodeCache, rank is special, use MutexLockerEx extern Mutex* MethodData_lock; // a lock on installation of method data extern Mutex* RetData_lock; // a lock on installation of RetData inside method data --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/src/share/vm/gc_implementation/g1/stringDedup.cpp 2014-03-07 14:31:36.466442503 +0100 @@ -0,0 +1,1427 @@ +/* + * Copyright (c) 2014, 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 "precompiled.hpp" +#include "classfile/altHashing.hpp" +#include "classfile/javaClasses.hpp" +#include "gc_implementation/g1/g1CollectedHeap.hpp" +#include "gc_implementation/g1/g1CollectedHeap.inline.hpp" +#include "gc_implementation/g1/g1GCPhaseTimes.hpp" +#include "gc_implementation/g1/g1Log.hpp" +#include "gc_implementation/g1/g1SATBCardTableModRefBS.hpp" +#include "gc_implementation/g1/stringDedup.hpp" +#include "gc_implementation/shared/concurrentGCThread.hpp" +#include "memory/allocation.hpp" +#include "memory/gcLocker.hpp" +#include "memory/padded.inline.hpp" +#include "oops/typeArrayOop.hpp" +#include "runtime/mutexLocker.hpp" +#include "utilities/stack.hpp" +#include "utilities/stack.inline.hpp" + +#define STRDEDUP_OBJECTS_FORMAT UINTX_FORMAT_W(12) +#define STRDEDUP_TIME_FORMAT "%1.7lf secs" +#define STRDEDUP_PERCENT_FORMAT "%5.1lf%%" +#define STRDEDUP_PERCENT_FORMAT_NS "%.1lf%%" +#define STRDEDUP_BYTES_FORMAT "%8.1lf%s" +#define STRDEDUP_BYTES_FORMAT_NS "%.1lf%s" +#define STRDEDUP_BYTES_PARAM(bytes) byte_size_in_proper_unit((double)(bytes)), proper_unit_for_byte_size((bytes)) + +class StringDedupStat : public StackObj { +private: + uintx _inspected; + uintx _skipped; + uintx _hashed; + uintx _known; + uintx _new; + uintx _new_bytes; + uintx _deduped; + uintx _deduped_bytes; + uintx _deduped_young; + uintx _deduped_young_bytes; + uintx _deduped_old; + uintx _deduped_old_bytes; + uintx _idle; + uintx _exec; + uintx _block; + double _start; + double _idle_elapsed; + double _exec_elapsed; + double _block_elapsed; + +public: + StringDedupStat(); + + void inc_inspected() { + _inspected++; + } + + void inc_skipped() { + _skipped++; + } + + void inc_hashed() { + _hashed++; + } + + void inc_known() { + _known++; + } + + void inc_new(uintx bytes) { + _new++; + _new_bytes += bytes; + } + + void inc_deduped_young(uintx bytes) { + _deduped++; + _deduped_bytes += bytes; + _deduped_young++; + _deduped_young_bytes += bytes; + } + + void inc_deduped_old(uintx bytes) { + _deduped++; + _deduped_bytes += bytes; + _deduped_old++; + _deduped_old_bytes += bytes; + } + + void mark_idle() { + _start = os::elapsedTime(); + _idle++; + } + + void mark_exec() { + double now = os::elapsedTime(); + _idle_elapsed = now - _start; + _start = now; + _exec++; + } + + void mark_block() { + double now = os::elapsedTime(); + _exec_elapsed += now - _start; + _start = now; + _block++; + } + + void mark_unblock() { + double now = os::elapsedTime(); + _block_elapsed += now - _start; + _start = now; + } + + void mark_done() { + double now = os::elapsedTime(); + _exec_elapsed += now - _start; + } + + StringDedupStat& operator+=(const StringDedupStat& stat); + + static void print_summary(outputStream* st, const StringDedupStat& last_stat, const StringDedupStat& total_stat); + static void print_statistics(outputStream* st, const StringDedupStat& stat, bool total); +}; + +StringDedupStat::StringDedupStat() : + _inspected(0), + _skipped(0), + _hashed(0), + _known(0), + _new(0), + _new_bytes(0), + _deduped(0), + _deduped_bytes(0), + _deduped_young(0), + _deduped_young_bytes(0), + _deduped_old(0), + _deduped_old_bytes(0), + _idle(0), + _exec(0), + _block(0), + _start(0.0), + _idle_elapsed(0.0), + _exec_elapsed(0.0), + _block_elapsed(0.0) { +} + +StringDedupStat& StringDedupStat::operator+=(const StringDedupStat& stat) { + _inspected += stat._inspected; + _skipped += stat._skipped; + _hashed += stat._hashed; + _known += stat._known; + _new += stat._new; + _new_bytes += stat._new_bytes; + _deduped += stat._deduped; + _deduped_bytes += stat._deduped_bytes; + _deduped_young += stat._deduped_young; + _deduped_young_bytes += stat._deduped_young_bytes; + _deduped_old += stat._deduped_old; + _deduped_old_bytes += stat._deduped_old_bytes; + _idle += stat._idle; + _exec += stat._exec; + _block += stat._block; + _idle_elapsed += stat._idle_elapsed; + _exec_elapsed += stat._exec_elapsed; + _block_elapsed += stat._block_elapsed; + return *this; +} + +void StringDedupStat::print_summary(outputStream* st, const StringDedupStat& last_stat, const StringDedupStat& total_stat) { + double total_deduped_bytes_percent = 0.0; + + if (total_stat._new_bytes > 0) { + // Avoid division by zero + total_deduped_bytes_percent = (double)total_stat._deduped_bytes / (double)total_stat._new_bytes * 100.0; + } + + st->date_stamp(PrintGCDateStamps); + st->stamp(PrintGCTimeStamps); + st->print_cr( + "[GC concurrent-string-deduplication, " + STRDEDUP_BYTES_FORMAT_NS"->"STRDEDUP_BYTES_FORMAT_NS"("STRDEDUP_BYTES_FORMAT_NS"), avg " + STRDEDUP_PERCENT_FORMAT_NS", "STRDEDUP_TIME_FORMAT"]", + STRDEDUP_BYTES_PARAM(last_stat._new_bytes), + STRDEDUP_BYTES_PARAM(last_stat._new_bytes - last_stat._deduped_bytes), + STRDEDUP_BYTES_PARAM(last_stat._deduped_bytes), + total_deduped_bytes_percent, + last_stat._exec_elapsed); +} + +void StringDedupStat::print_statistics(outputStream* st, const StringDedupStat& stat, bool total) { + double young_percent = 0.0; + double old_percent = 0.0; + double skipped_percent = 0.0; + double hashed_percent = 0.0; + double known_percent = 0.0; + double new_percent = 0.0; + double deduped_percent = 0.0; + double deduped_bytes_percent = 0.0; + double deduped_young_percent = 0.0; + double deduped_young_bytes_percent = 0.0; + double deduped_old_percent = 0.0; + double deduped_old_bytes_percent = 0.0; + + if (stat._inspected > 0) { + // Avoid division by zero + skipped_percent = (double)stat._skipped / (double)stat._inspected * 100.0; + hashed_percent = (double)stat._hashed / (double)stat._inspected * 100.0; + known_percent = (double)stat._known / (double)stat._inspected * 100.0; + new_percent = (double)stat._new / (double)stat._inspected * 100.0; + } + + if (stat._new > 0) { + // Avoid division by zero + deduped_percent = (double)stat._deduped / (double)stat._new * 100.0; + } + + if (stat._deduped > 0) { + // Avoid division by zero + deduped_young_percent = (double)stat._deduped_young / (double)stat._deduped * 100.0; + deduped_old_percent = (double)stat._deduped_old / (double)stat._deduped * 100.0; + } + + if (stat._new_bytes > 0) { + // Avoid division by zero + deduped_bytes_percent = (double)stat._deduped_bytes / (double)stat._new_bytes * 100.0; + } + + if (stat._deduped_bytes > 0) { + // Avoid division by zero + deduped_young_bytes_percent = (double)stat._deduped_young_bytes / (double)stat._deduped_bytes * 100.0; + deduped_old_bytes_percent = (double)stat._deduped_old_bytes / (double)stat._deduped_bytes * 100.0; + } + + if (total) { + st->print_cr( + " [Total Exec: "UINTX_FORMAT"/"STRDEDUP_TIME_FORMAT", Idle: "UINTX_FORMAT"/"STRDEDUP_TIME_FORMAT", Blocked: "UINTX_FORMAT"/"STRDEDUP_TIME_FORMAT"]", + stat._exec, stat._exec_elapsed, stat._idle, stat._idle_elapsed, stat._block, stat._block_elapsed); + } else { + st->print_cr( + " [Last Exec: "STRDEDUP_TIME_FORMAT", Idle: "STRDEDUP_TIME_FORMAT", Blocked: "UINTX_FORMAT"/"STRDEDUP_TIME_FORMAT"]", + stat._exec_elapsed, stat._idle_elapsed, stat._block, stat._block_elapsed); + } + st->print_cr( + " [Inspected: "STRDEDUP_OBJECTS_FORMAT"]\n" + " [Skipped: "STRDEDUP_OBJECTS_FORMAT"("STRDEDUP_PERCENT_FORMAT")]\n" + " [Hashed: "STRDEDUP_OBJECTS_FORMAT"("STRDEDUP_PERCENT_FORMAT")]\n" + " [Known: "STRDEDUP_OBJECTS_FORMAT"("STRDEDUP_PERCENT_FORMAT")]\n" + " [New: "STRDEDUP_OBJECTS_FORMAT"("STRDEDUP_PERCENT_FORMAT") "STRDEDUP_BYTES_FORMAT"]\n" + " [Deduplicated: "STRDEDUP_OBJECTS_FORMAT"("STRDEDUP_PERCENT_FORMAT") "STRDEDUP_BYTES_FORMAT"("STRDEDUP_PERCENT_FORMAT")]\n" + " [Young: "STRDEDUP_OBJECTS_FORMAT"("STRDEDUP_PERCENT_FORMAT") "STRDEDUP_BYTES_FORMAT"("STRDEDUP_PERCENT_FORMAT")]\n" + " [Old: "STRDEDUP_OBJECTS_FORMAT"("STRDEDUP_PERCENT_FORMAT") "STRDEDUP_BYTES_FORMAT"("STRDEDUP_PERCENT_FORMAT")]", + stat._inspected, + stat._skipped, skipped_percent, + stat._hashed, hashed_percent, + stat._known, known_percent, + stat._new, new_percent, STRDEDUP_BYTES_PARAM(stat._new_bytes), + stat._deduped, deduped_percent, STRDEDUP_BYTES_PARAM(stat._deduped_bytes), deduped_bytes_percent, + stat._deduped_young, deduped_young_percent, STRDEDUP_BYTES_PARAM(stat._deduped_young_bytes), deduped_young_bytes_percent, + stat._deduped_old, deduped_old_percent, STRDEDUP_BYTES_PARAM(stat._deduped_old_bytes), deduped_old_bytes_percent); +} + +class StringDedupEntry : public CHeapObj { +private: + StringDedupEntry* _next; + unsigned int _hash; + typeArrayOop _obj; + +public: + StringDedupEntry() : + _next(NULL), + _hash(0), + _obj(NULL) { + } + + StringDedupEntry* next() { + return _next; + } + + StringDedupEntry** next_addr() { + return &_next; + } + + void set_next(StringDedupEntry* next) { + _next = next; + } + + unsigned int hash() { + return _hash; + } + + void set_hash(unsigned int hash) { + _hash = hash; + } + + typeArrayOop obj() { + return _obj; + } + + typeArrayOop* obj_addr() { + return &_obj; + } + + void set_obj(typeArrayOop obj) { + _obj = obj; + } +}; + +class StringDedupEntryList : public CHeapObj { +private: + StringDedupEntry* _list; + size_t _length; + +public: + StringDedupEntryList() : + _list(NULL), + _length(0) { + } + + void add(StringDedupEntry* entry) { + entry->set_next(_list); + _list = entry; + _length++; + } + + StringDedupEntry* remove() { + StringDedupEntry* entry = _list; + if (entry != NULL) { + _list = entry->next(); + _length--; + } + return entry; + } + + size_t length() { + return _length; + } +}; + +class StringDedupCache : public AllStatic { +private: + // The table entry cache allows for single-threaded allocations + // and multi-threaded frees. Allocations are synchronized by + // StringDedupTable_lock as part of a table modification. + + // One free list per GC worker to allow lock less freeing of + // entries while doing a parallel scan of the table. Using + // PaddedEnd to avoid false sharing. + static PaddedEnd* _lists; + static size_t _nlists; + +public: + static void create(); + static StringDedupEntry* alloc(); + static void free(StringDedupEntry* entry, uint worker_id); + static void trim(size_t max_size); +}; + +PaddedEnd* StringDedupCache::_lists = NULL; +size_t StringDedupCache::_nlists = 0; + +void StringDedupCache::create() { + assert(UseStringDeduplication, "String deduplication not enabled"); + assert(_lists == NULL, "One string deduplication entry cache allowed"); + _nlists = MAX2(ParallelGCThreads, (size_t)1); + _lists = PaddedArray::create_unfreeable((uint)_nlists); +} + +StringDedupEntry* StringDedupCache::alloc() { + for (size_t i = 0; i < _nlists; i++) { + StringDedupEntry* entry = _lists[i].remove(); + if (entry != NULL) { + return entry; + } + } + return new StringDedupEntry(); +} + +void StringDedupCache::free(StringDedupEntry* entry, uint worker_id) { + assert(entry->obj() != NULL, "Double free"); + assert(worker_id < _nlists, "Invalid worker id"); + entry->set_obj(NULL); + entry->set_hash(0); + _lists[worker_id].add(entry); +} + +void StringDedupCache::trim(size_t max_size) { + size_t cache_size = 0; + for (size_t i = 0; i < _nlists; i++) { + StringDedupEntryList* list = &_lists[i]; + cache_size += list->length(); + while (cache_size > max_size) { + StringDedupEntry* entry = list->remove(); + assert(entry != NULL, "Should not be null"); + cache_size--; + delete entry; + } + } +} + +class StringDedupTable; + +class StringDedupClosure : public StackObj { +private: + BoolObjectClosure* _is_alive; + OopClosure* _keep_alive; + StringDedupTable* _resized_table; + StringDedupTable* _rehashed_table; + size_t _next_queue; + size_t _next_bucket; + +public: + StringDedupClosure(BoolObjectClosure* is_alive, OopClosure* keep_alive, bool allow_resize_and_rehash); + ~StringDedupClosure(); + + bool is_resizing() { + return _resized_table != NULL; + } + + StringDedupTable* resized_table() { + return _resized_table; + } + + bool is_rehashing() { + return _rehashed_table != NULL; + } + + size_t claim_queue() { + return (size_t)Atomic::add_ptr(1, &_next_queue) - 1; + } + + size_t claim_table_partition(size_t partition_size) { + return (size_t)Atomic::add_ptr(partition_size, &_next_bucket) - partition_size; + } + + bool is_alive(oop o) { + if (_is_alive != NULL) { + return _is_alive->do_object_b(o); + } + return true; + } + + void keep_alive(oop* p) { + if (_keep_alive != NULL) { + _keep_alive->do_oop(p); + } + } +}; + +class StringDedupTable : public CHeapObj { +private: + static StringDedupTable* _table; + static const size_t _min_size; + static const size_t _max_size; + static const double _grow_load_factor; + static const double _shrink_load_factor; + static const double _max_cache_factor; + static uintx _entries; + static uintx _entries_added; + static uintx _entries_removed; + static uintx _resize_count; + static uintx _rehash_count; + static const uintx _rehash_multiple; + static uintx _rehash_threshold; + static jint _hash_seed; + + StringDedupEntry** _buckets; + size_t _size; + uintx _shrink_threshold; + uintx _grow_threshold; + bool _rehash_needed; + + StringDedupTable(size_t size); + ~StringDedupTable(); + + StringDedupEntry** bucket(size_t index) { + return _buckets + index; + } + + size_t hash_to_index(unsigned int hash) { + return (size_t)hash & (_size - 1); + } + + void add(typeArrayOop value, unsigned int hash, StringDedupEntry** list); + void remove(StringDedupEntry** pentry, uint worker_id); + void transfer(StringDedupEntry** pentry, StringDedupTable* dest); + + typeArrayOop lookup(typeArrayOop value, unsigned int hash, + StringDedupEntry** list, uintx &count); + + typeArrayOop lookup_or_add_inner(typeArrayOop value, unsigned int hash); + + static typeArrayOop lookup_or_add(typeArrayOop value, unsigned int hash) { + // Protect the table from concurrent access. Also note that this lock + // acts as a fence for _table, which could have been replaced by a new + // instance if the table was resized or rehashed. + MutexLockerEx ml(StringDedupTable_lock, Mutex::_no_safepoint_check_flag); + return _table->lookup_or_add_inner(value, hash); + } + + static bool use_java_hash() { + return _hash_seed == 0; + } + + static bool equals(typeArrayOop value1, typeArrayOop value2); + + static unsigned int hash_code(typeArrayOop value); + + static uintx unlink_or_oops_do(StringDedupClosure* cl, + size_t partition_begin, + size_t partition_end, + uint worker_id); + +public: + static void create(); + + static void deduplicate(oop java_string, StringDedupStat& stat); + + static StringDedupTable* prepare_resize(); + static void finish_resize(StringDedupTable* resized_table); + + static StringDedupTable* prepare_rehash(); + static void finish_rehash(StringDedupTable* rehashed_table); + + static void unlink_or_oops_do(StringDedupClosure* cl, uint worker_id); + + static void trim_cache(); + + static void print_statistics(outputStream* st); + static void verify(); +}; + +StringDedupTable* StringDedupTable::_table = NULL; +const size_t StringDedupTable::_min_size = (1 << 10); // 1024 (Must be power of two) +const size_t StringDedupTable::_max_size = (1 << 24); // 16777216 (Must be power of two) +const double StringDedupTable::_grow_load_factor = 2.0; // Grow table at 200% load +const double StringDedupTable::_shrink_load_factor = _grow_load_factor / 3.0; // Shrink table at 67% load +const double StringDedupTable::_max_cache_factor = 0.1; // Cache a maximum of 10% of the table size +uintx StringDedupTable::_entries = 0; +uintx StringDedupTable::_entries_added = 0; +uintx StringDedupTable::_entries_removed = 0; +uintx StringDedupTable::_resize_count = 0; +uintx StringDedupTable::_rehash_count = 0; +const uintx StringDedupTable::_rehash_multiple = 60; // Hash bucket has 60 times more collisions than expected +uintx StringDedupTable::_rehash_threshold = (uintx)(_rehash_multiple * _grow_load_factor); +jint StringDedupTable::_hash_seed = 0; // A zero hash seed means we will use the Java hash function + +StringDedupTable::StringDedupTable(size_t size) : + _size(size), + _grow_threshold((uintx)(size * _grow_load_factor)), + _shrink_threshold((uintx)(size * _shrink_load_factor)), + _rehash_needed(false) { + _buckets = NEW_C_HEAP_ARRAY(StringDedupEntry*, _size, mtGC); + memset(_buckets, 0, _size * sizeof(StringDedupEntry*)); +} + +StringDedupTable::~StringDedupTable() { + FREE_C_HEAP_ARRAY(StringDedupEntry*, _buckets, mtGC); +} + +void StringDedupTable::create() { + assert(UseStringDeduplication, "String deduplication not enabled"); + assert(_table == NULL, "One string deduplication table allowed"); + _table = new StringDedupTable(_min_size); +} + +void StringDedupTable::add(typeArrayOop value, unsigned int hash, StringDedupEntry** list) { + StringDedupEntry* entry = StringDedupCache::alloc(); + entry->set_obj(value); + entry->set_hash(hash); + entry->set_next(*list); + *list = entry; +} + +void StringDedupTable::remove(StringDedupEntry** pentry, uint worker_id) { + StringDedupEntry* entry = *pentry; + *pentry = entry->next(); + StringDedupCache::free(entry, worker_id); +} + +void StringDedupTable::transfer(StringDedupEntry** pentry, StringDedupTable* dest) { + StringDedupEntry* entry = *pentry; + *pentry = entry->next(); + unsigned int hash = entry->hash(); + size_t index = dest->hash_to_index(hash); + StringDedupEntry** list = dest->bucket(index); + entry->set_next(*list); + *list = entry; +} + +bool StringDedupTable::equals(typeArrayOop value1, typeArrayOop value2) { + return (value1 == value2 || + (value1->length() == value2->length() && + (!memcmp(value1->base(T_CHAR), + value2->base(T_CHAR), + value1->length() * sizeof(jchar))))); +} + +typeArrayOop StringDedupTable::lookup(typeArrayOop value, unsigned int hash, + StringDedupEntry** list, uintx &count) { + for (StringDedupEntry* entry = *list; entry != NULL; entry = entry->next()) { + if (entry->hash() == hash) { + typeArrayOop existing_value = entry->obj(); + if (equals(value, existing_value)) { + // Match found + return existing_value; + } + } + count++; + } + + // Not found + return NULL; +} + +typeArrayOop StringDedupTable::lookup_or_add_inner(typeArrayOop value, unsigned int hash) { + size_t index = hash_to_index(hash); + StringDedupEntry** list = bucket(index); + uintx count = 0; + + // Lookup in list + typeArrayOop existing_value = lookup(value, hash, list, count); + + // Check if rehash is needed + if (count > _rehash_threshold) { + _rehash_needed = true; + } + + if (existing_value == NULL) { + // Not found, add new entry + add(value, hash, list); + + // Update statistics + _entries++; + _entries_added++; + } + + return existing_value; +} + +unsigned int StringDedupTable::hash_code(typeArrayOop value) { + unsigned int hash; + int length = value->length(); + const jchar* data = (jchar*)value->base(T_CHAR); + + if (use_java_hash()) { + hash = java_lang_String::hash_code(data, length); + } else { + hash = AltHashing::murmur3_32(_hash_seed, data, length); + } + + return hash; +} + +void StringDedupTable::deduplicate(oop java_string, StringDedupStat& stat) { + assert(UseStringDeduplication, "String deduplication not enabled"); + assert(java_lang_String::is_instance(java_string), "Must be a string"); + No_Safepoint_Verifier nsv; + + stat.inc_inspected(); + + typeArrayOop value = java_lang_String::value(java_string); + if (value == NULL) { + // String has no value + stat.inc_skipped(); + return; + } + + unsigned int hash = 0; + + if (use_java_hash()) { + // Get hash code from cache + hash = java_lang_String::hash(java_string); + } + + if (hash == 0) { + // Compute hash + hash = hash_code(value); + stat.inc_hashed(); + } + + if (use_java_hash() && hash != 0) { + // Store hash code in cache + java_lang_String::set_hash(java_string, hash); + } + + typeArrayOop existing_value = lookup_or_add(value, hash); + if (existing_value == value) { + // Same value, already known + stat.inc_known(); + return; + } + + // Get size of value array + uintx size_in_bytes = value->size() * HeapWordSize; + stat.inc_new(size_in_bytes); + + if (existing_value != NULL) { + // Enqueue the reference to make sure it is kept alive. Concurrent mark might + // otherwise declare it dead if there are no other strong references to this object. + G1SATBCardTableModRefBS::enqueue(existing_value); + + // Existing value found, deduplicate string + java_lang_String::set_value(java_string, existing_value); + + if (G1CollectedHeap::heap()->is_in_young(value)) { + stat.inc_deduped_young(size_in_bytes); + } else { + stat.inc_deduped_old(size_in_bytes); + } + } +} + +StringDedupTable* StringDedupTable::prepare_resize() { + size_t size = _table->_size; + + // Check if the hashtable needs to be resized + if (_entries > _table->_grow_threshold) { + // Grow table, double the size (table size must a power of two) + size *= 2; + if (size > _max_size) { + // Too big, don't resize + return NULL; + } + } else if (_entries < _table->_shrink_threshold) { + // Shrink table, half the size (table size must a power of two) + size /= 2; + if (size < _min_size) { + // Too small, don't resize + return NULL; + } + } else if (StringDeduplicationResizeALot) { + // Force grow + size *= 2; + if (size > _max_size) { + // Too big, force shrink instead + size /= 4; + } + } else { + // Resize not needed + return NULL; + } + + // Update statistics + _resize_count++; + + // Allocate the new table. The new table will be populated by workers + // calling unlink_or_oops_do() and finally installed by finish_resize(). + return new StringDedupTable(size); +} + +void StringDedupTable::finish_resize(StringDedupTable* resized_table) { + assert(resized_table != NULL, "Invalid table"); + + // Free old table + delete _table; + + // Install new table + _table = resized_table; +} + +void StringDedupTable::unlink_or_oops_do(StringDedupClosure* cl, uint worker_id) { + // The table is divided into partitions to allow lockless parallel processing by + // multiple worker threads. A worker thread first claims a partition, which ensures + // exclusive access to that part of the table, then continues to process it. To allow + // shrinking of the table in parallel we also need to make sure that the same worker + // thread processes all partitions where entries will hash to the same destination + // partition. Since the table size is always a power of two and we always shrink by + // dividing the table in half, we know that for a given partition there is only one + // other partition whoes entries will hash to the same destination partition. That + // other partition is always the sibling partition in the second half of the table. + // For example, if the table is divided into 8 partitions, the sibling of partition 0 + // is partition 4, the sibling of partition 1 is partition 5, etc. + size_t table_half = _table->_size / 2; + + // Let each partition be one page worth of buckets + size_t partition_size = MIN2(table_half, os::vm_page_size() / sizeof(StringDedupEntry*)); + assert(table_half % partition_size == 0, "Invalid partition size"); + + // Number of entries removed during the scan + uintx removed = 0; + + for (;;) { + // Grab next partition to scan + size_t partition_begin = cl->claim_table_partition(partition_size); + size_t partition_end = partition_begin + partition_size; + if (partition_begin >= table_half) { + // End of table + break; + } + + // Scan the partition followed by the sibling partition in the second half of the table + removed += unlink_or_oops_do(cl, partition_begin, partition_end, worker_id); + removed += unlink_or_oops_do(cl, table_half + partition_begin, table_half + partition_end, worker_id); + } + + // Delayed update of statistics to avoid contention on the table lock + if (removed > 0) { + MutexLockerEx ml(StringDedupTable_lock, Mutex::_no_safepoint_check_flag); + _entries -= removed; + _entries_removed += removed; + } +} + +uintx StringDedupTable::unlink_or_oops_do(StringDedupClosure* cl, + size_t partition_begin, + size_t partition_end, + uint worker_id) { + uintx removed = 0; + for (size_t bucket = partition_begin; bucket < partition_end; bucket++) { + StringDedupEntry** entry = _table->bucket(bucket); + while (*entry != NULL) { + oop* p = (oop*)(*entry)->obj_addr(); + if (cl->is_alive(*p)) { + cl->keep_alive(p); + if (cl->is_resizing()) { + // We are resizing the table, transfer entry to the new table + _table->transfer(entry, cl->resized_table()); + } else { + if (cl->is_rehashing()) { + // We are rehashing the table, rehash the entry but keep it + // in the table. We can't transfer entries into the new table + // at this point since we don't have exclusive access to all + // destination partitions. finish_rehash() will do a single + // threaded transfer of all entries. + typeArrayOop value = (typeArrayOop)*p; + unsigned int hash = hash_code(value); + (*entry)->set_hash(hash); + } + + // Move to next entry + entry = (*entry)->next_addr(); + } + } else { + // Not alive, remove entry from table + _table->remove(entry, worker_id); + removed++; + } + } + } + + return removed; +} + +StringDedupTable* StringDedupTable::prepare_rehash() { + if (!_table->_rehash_needed && !StringDeduplicationRehashALot) { + // Rehash not needed + return NULL; + } + + // Update statistics + _rehash_count++; + + // Compute new hash seed + _hash_seed = AltHashing::compute_seed(); + + // Allocate the new table, same size + return new StringDedupTable(_table->_size); +} + +void StringDedupTable::finish_rehash(StringDedupTable* rehashed_table) { + assert(rehashed_table != NULL, "Invalid table"); + + // Move all newly rehashed entries into the correct buckets in the new table + for (size_t bucket = 0; bucket < _table->_size; bucket++) { + StringDedupEntry** entry = _table->bucket(bucket); + while (*entry != NULL) { + _table->transfer(entry, rehashed_table); + } + } + + // Free old table + delete _table; + + // Install new table + _table = rehashed_table; +} + +void StringDedupTable::verify() { + assert(UseStringDeduplication, "String deduplication not enabled"); + for (size_t bucket = 0; bucket < _table->_size; bucket++) { + // Verify entries + StringDedupEntry** entry = _table->bucket(bucket); + while (*entry != NULL) { + typeArrayOop value = (*entry)->obj(); + guarantee(value != NULL, "Object must not be NULL"); + guarantee(Universe::heap()->is_in_reserved(value), "Object must be on the heap"); + guarantee(!value->is_forwarded(), "Object must not be forwarded"); + guarantee(value->is_typeArray(), "Object must be a typeArrayOop"); + unsigned int hash = hash_code(value); + guarantee((*entry)->hash() == hash, "Table entry has inorrect hash"); + guarantee(_table->hash_to_index(hash) == bucket, "Table entry has incorrect index"); + entry = (*entry)->next_addr(); + } + + // Verify that we do not have entries with identical oops or identical arrays. + // We only need to compare entries in the same bucket. If the same oop or an + // identical array has been inserted more than once into different/incorrect + // buckets the verification step above will catch that. + StringDedupEntry** entry1 = _table->bucket(bucket); + while (*entry1 != NULL) { + typeArrayOop value1 = (*entry1)->obj(); + StringDedupEntry** entry2 = (*entry1)->next_addr(); + while (*entry2 != NULL) { + typeArrayOop value2 = (*entry2)->obj(); + guarantee(!equals(value1, value2), "Table entries must not have identical arrays"); + entry2 = (*entry2)->next_addr(); + } + entry1 = (*entry1)->next_addr(); + } + } +} + +void StringDedupTable::trim_cache() { + MutexLockerEx ml(StringDedupTable_lock, Mutex::_no_safepoint_check_flag); + size_t max_cache_size = (size_t)(_table->_size * _max_cache_factor); + StringDedupCache::trim(max_cache_size); +} + +void StringDedupTable::print_statistics(outputStream* st) { + st->print_cr( + " [Table]\n" + " [Memory Usage: "STRDEDUP_BYTES_FORMAT_NS"]\n" + " [Size: "SIZE_FORMAT", Min: "SIZE_FORMAT", Max: "SIZE_FORMAT"]\n" + " [Entries: "UINTX_FORMAT", Load: "STRDEDUP_PERCENT_FORMAT_NS", Added: "UINTX_FORMAT", Removed: "UINTX_FORMAT"]\n" + " [Resize Count: "UINTX_FORMAT", Shrink Threshold: "UINTX_FORMAT"("STRDEDUP_PERCENT_FORMAT_NS"), Grow Threshold: "UINTX_FORMAT"("STRDEDUP_PERCENT_FORMAT_NS")]\n" + " [Rehash Count: "UINTX_FORMAT", Rehash Threshold: "UINTX_FORMAT", Hash Seed: 0x%x]\n" + " [Age Threshold: "UINTX_FORMAT"]", + STRDEDUP_BYTES_PARAM(_table->_size * sizeof(StringDedupEntry*) + _entries * sizeof(StringDedupEntry)), + _table->_size, _min_size, _max_size, + _entries, (double)_entries / (double)_table->_size * 100.0, _entries_added, _entries_removed, + _resize_count, _table->_shrink_threshold, _shrink_load_factor * 100.0, _table->_grow_threshold, _grow_load_factor * 100.0, + _rehash_count, _rehash_threshold, _hash_seed, + StringDeduplicationAgeThreshold); +} + +class StringDedupQueue : public CHeapObj { +private: + typedef Stack OopToDedupQueue; + + static StringDedupQueue* _queue; + static const size_t _max_size; + static const size_t _max_cache_size; + + OopToDedupQueue* _queues; + size_t _nqueues; + size_t _cursor; + volatile bool _empty; + uintx _dropped; + + StringDedupQueue(); + ~StringDedupQueue(); + + static void unlink_or_oops_do(StringDedupClosure* cl, size_t queue); + +public: + static void create(); + + static void wait(); + static void push(uint worker_id, oop java_string); + static bool pop(oop& java_string); + + static void unlink_or_oops_do(StringDedupClosure* cl); + + static void print_statistics(outputStream* st); + static void verify(); +}; + +StringDedupQueue* StringDedupQueue::_queue = NULL; +const size_t StringDedupQueue::_max_size = 1000000; // Max number of elements per queue +const size_t StringDedupQueue::_max_cache_size = 0; // Max cache size per queue + +StringDedupQueue::StringDedupQueue() : + _cursor(0), + _empty(true), + _dropped(0) { + _nqueues = MAX2(ParallelGCThreads, (size_t)1); + _queues = NEW_C_HEAP_ARRAY(OopToDedupQueue, _nqueues, mtGC); + for (size_t i = 0; i < _nqueues; i++) { + new (_queues + i) OopToDedupQueue(OopToDedupQueue::default_segment_size(), _max_cache_size, _max_size); + } +} + +StringDedupQueue::~StringDedupQueue() { + ShouldNotReachHere(); +} + +void StringDedupQueue::create() { + assert(UseStringDeduplication, "String deduplication not enabled"); + assert(_queue == NULL, "One string deduplication queue allowed"); + _queue = new StringDedupQueue(); +} + +void StringDedupQueue::wait() { + MonitorLockerEx ml(StringDedupQueue_lock, Mutex::_no_safepoint_check_flag); + while (_queue->_empty) { + ml.wait(Mutex::_no_safepoint_check_flag); + } +} + +void StringDedupQueue::push(uint worker_id, oop java_string) { + assert(UseStringDeduplication, "String deduplication not enabled"); + assert(SafepointSynchronize::is_at_safepoint(), "Must be at safepoint"); + assert(worker_id < _queue->_nqueues, "Invalid queue"); + + // Push and notify waiter + OopToDedupQueue& worker_queue = _queue->_queues[worker_id]; + if (!worker_queue.is_full()) { + worker_queue.push(java_string); + if (_queue->_empty) { + MonitorLockerEx ml(StringDedupQueue_lock, Mutex::_no_safepoint_check_flag); + if (_queue->_empty) { + // Mark non-empty and notify waiter + _queue->_empty = false; + ml.notify(); + } + } + } else { + // Queue is full, drop the string and update the statistics + Atomic::inc_ptr(&_queue->_dropped); + } +} + +bool StringDedupQueue::pop(oop& java_string) { + assert(!SafepointSynchronize::is_at_safepoint(), "Must not be at safepoint"); + No_Safepoint_Verifier nsv; + + // Try all queues before giving up + for (size_t tries = 0; tries < _queue->_nqueues; tries++) { + // The cursor indicates where we left of last time + OopToDedupQueue* queue = &_queue->_queues[_queue->_cursor]; + while (!queue->is_empty()) { + oop obj = queue->pop(); + // The oop we pop can be NULL if it was marked + // dead. Just ignore those and pop the next oop. + if (obj != NULL) { + java_string = obj; + return true; + } + } + + // Try next queue + _queue->_cursor = (_queue->_cursor + 1) % _queue->_nqueues; + } + + // Mark empty + _queue->_empty = true; + + return false; +} + +void StringDedupQueue::unlink_or_oops_do(StringDedupClosure* cl) { + // A worker thread first claims a queue, which ensures exclusive + // access to that queue, then continues to process it. + for (;;) { + // Grab next queue to scan + size_t queue = cl->claim_queue(); + if (queue >= _queue->_nqueues) { + // End of queues + break; + } + + // Scan the queue + unlink_or_oops_do(cl, queue); + } +} + +void StringDedupQueue::unlink_or_oops_do(StringDedupClosure* cl, size_t queue) { + assert(queue < _queue->_nqueues, "Invalid queue"); + StackIterator iter(_queue->_queues[queue]); + while (!iter.is_empty()) { + oop* p = iter.next_addr(); + if (*p != NULL) { + if (cl->is_alive(*p)) { + cl->keep_alive(p); + } else { + // Clear dead reference + *p = NULL; + } + } + } +} + +void StringDedupQueue::print_statistics(outputStream* st) { + st->print_cr( + " [Queue]\n" + " [Dropped: "UINTX_FORMAT"]", _queue->_dropped); +} + +void StringDedupQueue::verify() { + assert(UseStringDeduplication, "String deduplication not enabled"); + for (size_t i = 0; i < _queue->_nqueues; i++) { + StackIterator iter(_queue->_queues[i]); + while (!iter.is_empty()) { + oop obj = iter.next(); + if (obj != NULL) { + guarantee(Universe::heap()->is_in_reserved(obj), "Object must be on the heap"); + guarantee(!obj->is_forwarded(), "Object must not be forwarded"); + guarantee(java_lang_String::is_instance(obj), "Object must be a String"); + } + } + } +} + +class StringDedupThread: public ConcurrentGCThread { +private: + static StringDedupThread* _thread; + + StringDedupThread(); + ~StringDedupThread(); + + void print(outputStream* st, const StringDedupStat& last_stat, const StringDedupStat& total_stat); + +public: + static void create(); + static StringDedupThread* thread(); + + virtual void run(); + virtual void print_on(outputStream* st) const; +}; + +StringDedupThread* StringDedupThread::_thread = NULL; + +StringDedupThread::StringDedupThread() : + ConcurrentGCThread() { + set_name("String Deduplication Thread"); + create_and_start(); +} + +StringDedupThread::~StringDedupThread() { + ShouldNotReachHere(); +} + +void StringDedupThread::create() { + assert(UseStringDeduplication, "String deduplication not enabled"); + assert(_thread == NULL, "One string deduplication thread allowed"); + _thread = new StringDedupThread(); +} + +StringDedupThread* StringDedupThread::thread() { + assert(UseStringDeduplication, "String deduplication not enabled"); + assert(_thread != NULL, "String deduplication thread not created"); + return _thread; +} + +void StringDedupThread::print_on(outputStream* st) const { + st->print("\"%s\" ", name()); + Thread::print_on(st); + st->cr(); +} + +void StringDedupThread::run() { + StringDedupStat total_stat; + + initialize_in_thread(); + wait_for_universe_init(); + + // Main loop + for (;;) { + StringDedupStat stat; + oop java_string; + + stat.mark_idle(); + + // Wait for the queue to become non-empty + StringDedupQueue::wait(); + + // Include this thread in safepoints + stsJoin(); + + stat.mark_exec(); + + // Process the queue + while (StringDedupQueue::pop(java_string)) { + StringDedupTable::deduplicate(java_string, stat); + + // Safepoint this thread if needed + if (stsShouldYield()) { + stat.mark_block(); + stsYield(NULL); + stat.mark_unblock(); + } + } + + // Trim table entry cache + StringDedupTable::trim_cache(); + + stat.mark_done(); + + // Print statistics + total_stat += stat; + print(gclog_or_tty, stat, total_stat); + + // Exclude this thread from safepoints + stsLeave(); + } + + ShouldNotReachHere(); +} + +void StringDedupThread::print(outputStream* st, const StringDedupStat& last_stat, const StringDedupStat& total_stat) { + if (G1Log::fine() || PrintStringDeduplicationStatistics) { + StringDedupStat::print_summary(st, last_stat, total_stat); + if (PrintStringDeduplicationStatistics) { + StringDedupStat::print_statistics(st, last_stat, false); + StringDedupStat::print_statistics(st, total_stat, true); + StringDedupTable::print_statistics(st); + StringDedupQueue::print_statistics(st); + } + } +} + +void StringDedup::initialize() { + if (UseStringDeduplication) { + StringDedupQueue::create(); + StringDedupCache::create(); + StringDedupTable::create(); + StringDedupThread::create(); + } +} + +/* + * Candidate selection + * + * An object is considered a deduplication candidate if all of the following + * statements are true: + * + * - The object is an instance of java.lang.String + * + * - The object is being evacuated from a young heap region + * + * - The object is being evacuated to a young/survivor heap region and the + * object's age is equal to the deduplication age threshold + * + * or + * + * The object is being evacuated to an old heap region and the object's age is + * less than the deduplication age threshold + * + * Once an string object has been promoted to an old region, or its age is higher + * than the deduplication age threshold, is will never become a candidate again. + * This approach avoids making the same object a candidate more than once. + * + * Interned strings are a bit special. They are explicitly deduplicated just before + * being inserted into the StringTable (to avoid counteracting C2 optimizations done + * on string literals), then they also become deduplication candidates if they reach + * the deduplication age threshold or is evacuated to an old heap region. The second + * appempt to deduplicate such strings will be in vain, but we have no fast way of + * filtering them out. This has not shown to be a problem, as the number of interned + * strings is usually dwarfed by the number of normal (non-interned) strings. + */ + +static bool is_candidate_from_mark(oop obj) { + if (java_lang_String::is_instance(obj)) { + bool from_young = G1CollectedHeap::heap()->heap_region_containing_raw(obj)->is_young(); + if (from_young && obj->age() < StringDeduplicationAgeThreshold) { + // Candidate found. String is being evacuated from young to old but has not + // reached the deduplication age threshold, i.e. has not previously been a + // candidate during its life in the young generation. + return true; + } + } + // Not a candicate + return false; +} + +void StringDedup::enqueue_from_mark(oop java_string) { + assert(UseG1GC, "String deduplication only available with G1"); + assert(UseStringDeduplication, "String deduplication not enabled"); + if (is_candidate_from_mark(java_string)) { + StringDedupQueue::push(0 /* worker_id */, java_string); + } +} + +static bool is_candidate_from_evacuation(bool from_young, bool to_young, oop obj) { + if (from_young && java_lang_String::is_instance(obj)) { + if (to_young && obj->age() == StringDeduplicationAgeThreshold) { + // Candidate found. String is being evacuated from young to young and just + // reached the deduplication age threshold. + return true; + } + if (!to_young && obj->age() < StringDeduplicationAgeThreshold) { + // Candidate found. String is being evacuated from young to old but has not + // reached the deduplication age threshold, i.e. has not previously been a + // candidate during its life in the young generation. + return true; + } + } + // Not a candicate + return false; +} + +void StringDedup::enqueue_from_evacuation(bool from_young, bool to_young, uint worker_id, oop java_string) { + assert(UseStringDeduplication, "String deduplication not enabled"); + if (is_candidate_from_evacuation(from_young, to_young, java_string)) { + StringDedupQueue::push(worker_id, java_string); + } +} + +void StringDedup::deduplicate(oop java_string) { + assert(UseG1GC, "String deduplication only available with G1"); + assert(UseStringDeduplication, "String deduplication not enabled"); + StringDedupStat dummy; // Statistics from this path is never used + StringDedupTable::deduplicate(java_string, dummy); +} + +StringDedupClosure::StringDedupClosure(BoolObjectClosure* is_alive, OopClosure* keep_alive, bool allow_resize_and_rehash) : + _is_alive(is_alive), + _keep_alive(keep_alive), + _resized_table(NULL), + _rehashed_table(NULL), + _next_queue(0), + _next_bucket(0) { + if (allow_resize_and_rehash) { + // If both resize and rehash is needed, only do resize. Rehash of + // the table will eventually happen if the situation persists. + _resized_table = StringDedupTable::prepare_resize(); + if (!is_resizing()) { + _rehashed_table = StringDedupTable::prepare_rehash(); + } + } +} + +StringDedupClosure::~StringDedupClosure() { + assert(!is_resizing() || !is_rehashing(), "Can not both resize and rehash"); + if (is_resizing()) { + StringDedupTable::finish_resize(_resized_table); + } else if (is_rehashing()) { + StringDedupTable::finish_rehash(_rehashed_table); + } +} + +void StringDedup::unlink(BoolObjectClosure* is_alive) { + // Don't allow a potential resize or rehash during unlink, as the unlink + // operation itself might remove enough entries to invalidate such a decision. + unlink_or_oops_do(is_alive, NULL, false /* allow_resize_and_rehash */); +} + +void StringDedup::oops_do(OopClosure* keep_alive) { + unlink_or_oops_do(NULL, keep_alive); +} + +class StringDedupTask : public AbstractGangTask { +private: + StringDedupClosure _cl; + +public: + StringDedupTask(BoolObjectClosure* is_alive, OopClosure* keep_alive, bool allow_resize_and_rehash) : + AbstractGangTask("StringDedupTask"), + _cl(is_alive, keep_alive, allow_resize_and_rehash) { + } + + virtual void work(uint worker_id) { + double queue_fixup_start = os::elapsedTime(); + StringDedupQueue::unlink_or_oops_do(&_cl); + + double table_fixup_start = os::elapsedTime(); + StringDedupTable::unlink_or_oops_do(&_cl, worker_id); + + double queue_fixup_time_ms = (table_fixup_start - queue_fixup_start) * 1000.0; + double table_fixup_time_ms = (os::elapsedTime() - table_fixup_start) * 1000.0; + G1CollectorPolicy* g1p = G1CollectedHeap::heap()->g1_policy(); + g1p->phase_times()->record_string_dedup_queue_fixup_worker_time(worker_id, queue_fixup_time_ms); + g1p->phase_times()->record_string_dedup_table_fixup_worker_time(worker_id, table_fixup_time_ms); + } +}; + +void StringDedup::unlink_or_oops_do(BoolObjectClosure* is_alive, OopClosure* keep_alive, bool allow_resize_and_rehash) { + if (UseStringDeduplication) { + G1CollectorPolicy* g1p = G1CollectedHeap::heap()->g1_policy(); + g1p->phase_times()->note_string_dedup_fixup_start(); + double fixup_start = os::elapsedTime(); + + StringDedupTask task(is_alive, keep_alive, allow_resize_and_rehash); + if (G1CollectedHeap::use_parallel_gc_threads()) { + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + g1h->set_par_threads(); + g1h->workers()->run_task(&task); + g1h->set_par_threads(0); + } else { + task.work(0); + } + + double fixup_time_ms = (os::elapsedTime() - fixup_start) * 1000.0; + g1p->phase_times()->record_string_dedup_fixup_time(fixup_time_ms); + g1p->phase_times()->note_string_dedup_fixup_end(); + } +} + +void StringDedup::threads_do(ThreadClosure* tc) { + if (UseStringDeduplication) { + tc->do_thread(StringDedupThread::thread()); + } +} + +void StringDedup::print_worker_threads_on(outputStream* st) { + if (UseStringDeduplication) { + StringDedupThread::thread()->print_on(st); + st->cr(); + } +} + +void StringDedup::verify() { + StringDedupQueue::verify(); + StringDedupTable::verify(); +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/src/share/vm/gc_implementation/g1/stringDedup.hpp 2014-03-07 14:31:37.157600861 +0100 @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2014, 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. + * + */ + +#ifndef SHARE_VM_GC_IMPLEMENTATION_SHARED_STRINGDEDUP_HPP +#define SHARE_VM_GC_IMPLEMENTATION_SHARED_STRINGDEDUP_HPP + +#include "memory/allocation.hpp" +#include "oops/oop.hpp" + +class OopClosure; +class BoolObjectClosure; +class ThreadClosure; +class outputStream; + +class StringDedup : public AllStatic { +public: + static void initialize(); + + static void deduplicate(oop java_string); + + static void enqueue_from_mark(oop java_string); + static void enqueue_from_evacuation(bool from_young, bool to_young, + unsigned int queue, oop java_string); + + static void unlink(BoolObjectClosure* is_alive); + static void oops_do(OopClosure* keep_alive); + static void unlink_or_oops_do(BoolObjectClosure* is_alive, OopClosure* keep_alive, + bool allow_resize_and_rehash = true); + + static void threads_do(ThreadClosure* tc); + static void print_worker_threads_on(outputStream* st); + static void verify(); +}; + +#endif // SHARE_VM_GC_IMPLEMENTATION_SHARED_STRINGDEDUP_HPP --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationAgeThreshold.java 2014-03-07 14:31:37.857605488 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationAgeThreshold + * @summary Test string deduplication age threshold + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationAgeThreshold { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testAgeThreshold(); + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationFullGC.java 2014-03-07 14:31:38.527598854 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationFullGC + * @summary Test string deduplication during full GC + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationFullGC { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testFullGC(); + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationInterned.java 2014-03-07 14:31:39.207563592 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationInterned + * @summary Test string deduplication of interned strings + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationInterned { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testInterned(); + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationMemoryUsage.java 2014-03-07 14:31:39.906315119 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationMemoryUsage + * @summary Test string deduplication memory usage + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationMemoryUsage { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testMemoryUsage(); + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationPrintOptions.java 2014-03-07 14:31:40.597565586 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationPrintOptions + * @summary Test string deduplication print options + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationPrintOptions { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testPrintOptions(); + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationTableRehash.java 2014-03-07 14:31:41.256582000 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationTableRehash + * @summary Test string deduplication table rehash + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationTableRehash { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testTableRehash(); + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationTableResize.java 2014-03-07 14:31:41.937008593 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationTableResize + * @summary Test string deduplication table resize + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationTableResize { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testTableResize(); + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationTools.java 2014-03-07 14:31:42.607584160 +0100 @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * Common code for string deduplication tests + */ + +import java.lang.management.*; +import java.lang.reflect.*; +import java.security.*; +import java.util.*; +import com.oracle.java.testlibrary.*; +import sun.misc.*; + +class TestStringDeduplicationTools { + private static final String YoungGC = "YoungGC"; + private static final String FullGC = "FullGC"; + + private static final int Xmn = 50; // MB + private static final int Xms = 100; // MB + private static final int Xmx = 100; // MB + private static final int MB = 1024 * 1024; + private static final int StringLength = 50; + + private static Field valueField; + private static Unsafe unsafe; + private static byte[] dummy; + + static { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + unsafe = (Unsafe)field.get(null); + + valueField = String.class.getDeclaredField("value"); + valueField.setAccessible(true); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Object getValue(String string) { + try { + return valueField.get(string); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void doFullGc(int numberOfTimes) { + for (int i = 0; i < numberOfTimes; i++) { + System.out.println("Begin: Full GC " + (i + 1) + "/" + numberOfTimes); + System.gc(); + System.out.println("End: Full GC " + (i + 1) + "/" + numberOfTimes); + } + } + + private static void doYoungGc(int numberOfTimes) { + // Provoke at least numberOfTimes young GCs + final int objectSize = 128; + final int maxObjectInYoung = (Xmn * MB) / objectSize; + for (int i = 0; i < numberOfTimes; i++) { + System.out.println("Begin: Young GC " + (i + 1) + "/" + numberOfTimes); + for (int j = 0; j < maxObjectInYoung + 1; j++) { + dummy = new byte[objectSize]; + } + System.out.println("End: Young GC " + (i + 1) + "/" + numberOfTimes); + } + } + + private static void forceDeduplication(int ageThreshold, String gcType) { + // Force deduplication to happen by either causing a FullGC or a YoungGC. + // We do several collections to also provoke a situation where the the + // deduplication thread needs to yield while processing the qeueue. This + // also tests that the references in the deduplication queue are adjusted + // accordingly. + if (gcType.equals(FullGC)) { + doFullGc(3); + } else { + doYoungGc(ageThreshold + 3); + } + } + + private static String generateString(int id) { + StringBuilder builder = new StringBuilder(StringLength); + + builder.append("DeduplicationTestString:" + id + ":"); + + while (builder.length() < StringLength) { + builder.append('X'); + } + + return builder.toString(); + } + + private static ArrayList createStrings(int total, int unique) { + System.out.println("Creating strings: total=" + total + ", unique=" + unique); + if (total % unique != 0) { + throw new RuntimeException("Total must be divisible by unique"); + } + + ArrayList list = new ArrayList(total); + for (int j = 0; j < total / unique; j++) { + for (int i = 0; i < unique; i++) { + list.add(generateString(i)); + } + } + + return list; + } + + private static void verifyStrings(ArrayList list, int uniqueExpected) { + for (;;) { + // Check number of deduplicated strings + ArrayList unique = new ArrayList(uniqueExpected); + for (String string: list) { + Object value = getValue(string); + boolean uniqueValue = true; + for (Object obj: unique) { + if (obj == value) { + uniqueValue = false; + break; + } + } + + if (uniqueValue) { + unique.add(value); + } + } + + System.out.println("Verifying strings: total=" + list.size() + + ", uniqueFound=" + unique.size() + + ", uniqueExpected=" + uniqueExpected); + + if (unique.size() == uniqueExpected) { + System.out.println("Deduplication completed"); + break; + } else { + System.out.println("Deduplication not completed, waiting..."); + + // Give the deduplication thread time to complete + try { + Thread.sleep(1000); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + + private static OutputAnalyzer runTest(String... extraArgs) throws Exception { + String[] defaultArgs = new String[] { + "-Xmn" + Xmn + "m", + "-Xms" + Xms + "m", + "-Xmx" + Xmx + "m", + "-XX:+UseG1GC", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+VerifyAfterGC" // Always verify after GC + }; + + ArrayList args = new ArrayList(); + args.addAll(Arrays.asList(defaultArgs)); + args.addAll(Arrays.asList(extraArgs)); + + ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(args.toArray(new String[args.size()])); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + System.err.println(output.getStderr()); + System.out.println(output.getStdout()); + return output; + } + + private static class DeduplicationTest { + public static void main(String[] args) { + System.out.println("Begin: DeduplicationTest"); + + final int numberOfStrings = Integer.parseUnsignedInt(args[0]); + final int numberOfUniqueStrings = Integer.parseUnsignedInt(args[1]); + final int ageThreshold = Integer.parseUnsignedInt(args[2]); + final String gcType = args[3]; + + ArrayList list = createStrings(numberOfStrings, numberOfUniqueStrings); + forceDeduplication(ageThreshold, gcType); + verifyStrings(list, numberOfUniqueStrings); + + System.out.println("End: DeduplicationTest"); + } + + public static OutputAnalyzer run(int numberOfStrings, int ageThreshold, String gcType, String... extraArgs) throws Exception { + String[] defaultArgs = new String[] { + "-XX:+UseStringDeduplication", + "-XX:StringDeduplicationAgeThreshold=" + ageThreshold, + DeduplicationTest.class.getName(), + "" + numberOfStrings, + "" + numberOfStrings / 2, + "" + ageThreshold, + gcType + }; + + ArrayList args = new ArrayList(); + args.addAll(Arrays.asList(extraArgs)); + args.addAll(Arrays.asList(defaultArgs)); + + return runTest(args.toArray(new String[args.size()])); + } + } + + private static class InternedTest { + public static void main(String[] args) { + // This test verifies that interned strings are always + // deduplicated when being interned, and never after + // being interned. + + System.out.println("Begin: InternedTest"); + + final int ageThreshold = Integer.parseUnsignedInt(args[0]); + final String baseString = "DeduplicationTestString:" + InternedTest.class.getName(); + + // Create duplicate of baseString + StringBuilder sb1 = new StringBuilder(baseString); + String dupString1 = sb1.toString(); + if (getValue(dupString1) == getValue(baseString)) { + throw new RuntimeException("Values should not match"); + } + + // Force baseString to be inspected for deduplication + // and be inserted into the deduplication hashtable. + forceDeduplication(ageThreshold, FullGC); + + // Wait for deduplication to occur + while (getValue(dupString1) != getValue(baseString)) { + System.out.println("Waiting..."); + try { + Thread.sleep(100); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // Create a new duplicate of baseString + StringBuilder sb2 = new StringBuilder(baseString); + String dupString2 = sb2.toString(); + if (getValue(dupString2) == getValue(baseString)) { + throw new RuntimeException("Values should not match"); + } + + // Intern the new duplicate + Object beforeInternedValue = getValue(dupString2); + String internedString = dupString2.intern(); + if (internedString != dupString2) { + throw new RuntimeException("String should match"); + } + if (getValue(internedString) != getValue(baseString)) { + throw new RuntimeException("Values should match"); + } + + // Check original value of interned string, to make sure + // deduplication happened on the interned string and not + // on the base string + if (beforeInternedValue == getValue(baseString)) { + throw new RuntimeException("Values should not match"); + } + + System.out.println("End: InternedTest"); + } + + public static OutputAnalyzer run() throws Exception { + return runTest("-XX:+PrintGC", + "-XX:+PrintGCDetails", + "-XX:+UseStringDeduplication", + "-XX:+PrintStringDeduplicationStatistics", + "-XX:StringDeduplicationAgeThreshold=" + DefaultAgeThreshold, + InternedTest.class.getName(), + "" + DefaultAgeThreshold); + } + } + + private static class MemoryUsageTest { + public static void main(String[] args) { + System.out.println("Begin: MemoryUsageTest"); + + final boolean useStringDeduplication = Boolean.parseBoolean(args[0]); + final int numberOfStrings = LargeNumberOfStrings; + final int numberOfUniqueStrings = 1; + + ArrayList list = createStrings(numberOfStrings, numberOfUniqueStrings); + forceDeduplication(DefaultAgeThreshold, FullGC); + + if (useStringDeduplication) { + verifyStrings(list, numberOfUniqueStrings); + } + + System.gc(); + System.out.println("Heap Memory Usage: " + ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed()); + + System.out.println("End: MemoryUsageTest"); + } + + public static OutputAnalyzer run(boolean useStringDeduplication) throws Exception { + String[] extraArgs = new String[0]; + + if (useStringDeduplication) { + extraArgs = new String[] { + "-XX:+UseStringDeduplication", + "-XX:+PrintStringDeduplicationStatistics", + "-XX:StringDeduplicationAgeThreshold=" + DefaultAgeThreshold + }; + } + + String[] defaultArgs = new String[] { + "-XX:+PrintGC", + "-XX:+PrintGCDetails", + MemoryUsageTest.class.getName(), + "" + useStringDeduplication + }; + + ArrayList args = new ArrayList(); + args.addAll(Arrays.asList(extraArgs)); + args.addAll(Arrays.asList(defaultArgs)); + + return runTest(args.toArray(new String[args.size()])); + } + } + + /* + * Tests + */ + + private static final int LargeNumberOfStrings = 10000; + private static final int SmallNumberOfStrings = 10; + + private static final int MaxAgeThreshold = 15; + private static final int DefaultAgeThreshold = 3; + private static final int MinAgeThreshold = 1; + + private static final int TooLowAgeThreshold = MinAgeThreshold - 1; + private static final int TooHighAgeThreshold = MaxAgeThreshold + 1; + + public static void testYoungGC() throws Exception { + // Do young GC to age strings to provoke deduplication + OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings, + DefaultAgeThreshold, + YoungGC, + "-XX:+PrintGC", + "-XX:+PrintStringDeduplicationStatistics"); + output.shouldNotContain("Full GC"); + output.shouldContain("GC pause (G1 Evacuation Pause) (young)"); + output.shouldContain("GC concurrent-string-deduplication"); + output.shouldContain("Deduplicated:"); + output.shouldHaveExitValue(0); + } + + public static void testFullGC() throws Exception { + // Do full GC to age strings to provoke deduplication + OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings, + DefaultAgeThreshold, + FullGC, + "-XX:+PrintGC", + "-XX:+PrintStringDeduplicationStatistics"); + output.shouldNotContain("GC pause (G1 Evacuation Pause) (young)"); + output.shouldContain("Full GC"); + output.shouldContain("GC concurrent-string-deduplication"); + output.shouldContain("Deduplicated:"); + output.shouldHaveExitValue(0); + } + + public static void testTableResize() throws Exception { + // Test with StringDeduplicationResizeALot + OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings, + DefaultAgeThreshold, + YoungGC, + "-XX:+PrintGC", + "-XX:+PrintStringDeduplicationStatistics", + "-XX:+StringDeduplicationResizeALot"); + output.shouldContain("GC concurrent-string-deduplication"); + output.shouldContain("Deduplicated:"); + output.shouldNotContain("Resize Count: 0"); + output.shouldHaveExitValue(0); + } + + public static void testTableRehash() throws Exception { + // Test with StringDeduplicationRehashALot + OutputAnalyzer output = DeduplicationTest.run(LargeNumberOfStrings, + DefaultAgeThreshold, + YoungGC, + "-XX:+PrintGC", + "-XX:+PrintStringDeduplicationStatistics", + "-XX:+StringDeduplicationRehashALot"); + output.shouldContain("GC concurrent-string-deduplication"); + output.shouldContain("Deduplicated:"); + output.shouldNotContain("Rehash Count: 0"); + output.shouldNotContain("Hash Seed: 0x0"); + output.shouldHaveExitValue(0); + } + + public static void testAgeThreshold() throws Exception { + OutputAnalyzer output; + + // Test with max age theshold + output = DeduplicationTest.run(SmallNumberOfStrings, + MaxAgeThreshold, + YoungGC, + "-XX:+PrintGC", + "-XX:+PrintStringDeduplicationStatistics"); + output.shouldContain("GC concurrent-string-deduplication"); + output.shouldContain("Deduplicated:"); + output.shouldHaveExitValue(0); + + // Test with min age theshold + output = DeduplicationTest.run(SmallNumberOfStrings, + MinAgeThreshold, + YoungGC, + "-XX:+PrintGC", + "-XX:+PrintStringDeduplicationStatistics"); + output.shouldContain("GC concurrent-string-deduplication"); + output.shouldContain("Deduplicated:"); + output.shouldHaveExitValue(0); + + // Test with too low age threshold + output = DeduplicationTest.run(SmallNumberOfStrings, + TooLowAgeThreshold, + YoungGC); + output.shouldContain("StringDeduplicationAgeThreshold of " + TooLowAgeThreshold + + " is invalid; must be between " + MinAgeThreshold + " and " + MaxAgeThreshold); + output.shouldHaveExitValue(1); + + // Test with too high age threshold + output = DeduplicationTest.run(SmallNumberOfStrings, + TooHighAgeThreshold, + YoungGC); + output.shouldContain("StringDeduplicationAgeThreshold of " + TooHighAgeThreshold + + " is invalid; must be between " + MinAgeThreshold + " and " + MaxAgeThreshold); + output.shouldHaveExitValue(1); + } + + public static void testPrintOptions() throws Exception { + OutputAnalyzer output; + + // Test without PrintGC and without PrintStringDeduplicationStatistics + output = DeduplicationTest.run(SmallNumberOfStrings, + DefaultAgeThreshold, + YoungGC); + output.shouldNotContain("GC concurrent-string-deduplication"); + output.shouldNotContain("Deduplicated:"); + output.shouldHaveExitValue(0); + + // Test with PrintGC but without PrintStringDeduplicationStatistics + output = DeduplicationTest.run(SmallNumberOfStrings, + DefaultAgeThreshold, + YoungGC, + "-XX:+PrintGC"); + output.shouldContain("GC concurrent-string-deduplication"); + output.shouldNotContain("Deduplicated:"); + output.shouldHaveExitValue(0); + } + + public static void testInterned() throws Exception { + // Test that interned strings are deduplicated before being interned + OutputAnalyzer output = InternedTest.run(); + output.shouldHaveExitValue(0); + } + + public static void testMemoryUsage() throws Exception { + // Test that memory usage is reduced after deduplication + OutputAnalyzer output; + final String usagePattern = "Heap Memory Usage: (\\d+)"; + + // Run without deduplication + output = MemoryUsageTest.run(false); + output.shouldHaveExitValue(0); + final long memoryUsageWithoutDedup = Long.parseLong(output.firstMatch(usagePattern, 1)); + + // Run with deduplication + output = MemoryUsageTest.run(true); + output.shouldHaveExitValue(0); + final long memoryUsageWithDedup = Long.parseLong(output.firstMatch(usagePattern, 1)); + + // Calculate expected memory usage with deduplication enabled. This calculation does + // not take alignment and padding into account, so it's a conservative estimate. + final long sizeOfChar = 2; // bytes + final long bytesSaved = (LargeNumberOfStrings - 1) * (StringLength * sizeOfChar + unsafe.ARRAY_CHAR_BASE_OFFSET); + final long memoryUsageWithDedupExpected = memoryUsageWithoutDedup - bytesSaved; + + System.out.println("Memory usage summary:"); + System.out.println(" memoryUsageWithoutDedup: " + memoryUsageWithoutDedup); + System.out.println(" memoryUsageWithDedup: " + memoryUsageWithDedup); + System.out.println(" memoryUsageWithDedupExpected: " + memoryUsageWithDedupExpected); + + if (memoryUsageWithDedup > memoryUsageWithDedupExpected) { + throw new Exception("Unexpected memory usage, memoryUsageWithDedup should less or equal to memoryUsageWithDedupExpected"); + } + } +} --- /dev/null 2014-01-08 17:25:58.840516250 +0100 +++ new/test/gc/g1/TestStringDeduplicationYoungGC.java 2014-03-07 14:31:43.327539000 +0100 @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014, 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. + */ + +/* + * @test TestStringDeduplicationYoungGC + * @summary Test string deduplication during young GC + * @key gc + * @library /testlibrary + */ + +public class TestStringDeduplicationYoungGC { + public static void main(String[] args) throws Exception { + TestStringDeduplicationTools.testYoungGC(); + } +}