# HG changeset patch # User redestad # Date 1518111930 -3600 # Node ID 3d17a524da950d58ec3cfb94746d62b386481906 # Parent 85c27aabf3124b41c86877a4315b42ee1aa1f143 8196869: Optimize Locale creation Reviewed-by: psandoz, plevart, naoto diff -r 85c27aabf312 -r 3d17a524da95 src/java.base/share/classes/java/util/Locale.java --- a/src/java.base/share/classes/java/util/Locale.java Thu Feb 08 10:06:57 2018 -0500 +++ b/src/java.base/share/classes/java/util/Locale.java Thu Feb 08 18:45:30 2018 +0100 @@ -808,17 +808,26 @@ } static Locale getInstance(BaseLocale baseloc, LocaleExtensions extensions) { - LocaleKey key = new LocaleKey(baseloc, extensions); - return LOCALECACHE.get(key); + if (extensions == null) { + return LOCALECACHE.get(baseloc); + } else { + LocaleKey key = new LocaleKey(baseloc, extensions); + return LOCALECACHE.get(key); + } } - private static class Cache extends LocaleObjectCache { + private static class Cache extends LocaleObjectCache { private Cache() { } @Override - protected Locale createObject(LocaleKey key) { - return new Locale(key.base, key.exts); + protected Locale createObject(Object key) { + if (key instanceof BaseLocale) { + return new Locale((BaseLocale)key, null); + } else { + LocaleKey lk = (LocaleKey)key; + return new Locale(lk.base, lk.exts); + } } } diff -r 85c27aabf312 -r 3d17a524da95 src/java.base/share/classes/sun/util/locale/BaseLocale.java --- a/src/java.base/share/classes/sun/util/locale/BaseLocale.java Thu Feb 08 10:06:57 2018 -0500 +++ b/src/java.base/share/classes/sun/util/locale/BaseLocale.java Thu Feb 08 18:45:30 2018 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2018, 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 @@ -31,8 +31,8 @@ */ package sun.util.locale; + import java.lang.ref.SoftReference; - import java.util.StringJoiner; public final class BaseLocale { @@ -48,26 +48,28 @@ private volatile int hash; - // This method must be called only when creating the Locale.* constants. - private BaseLocale(String language, String region) { - this.language = language; - this.script = ""; - this.region = region; - this.variant = ""; - } - - private BaseLocale(String language, String script, String region, String variant) { - this.language = (language != null) ? LocaleUtils.toLowerString(language).intern() : ""; - this.script = (script != null) ? LocaleUtils.toTitleString(script).intern() : ""; - this.region = (region != null) ? LocaleUtils.toUpperString(region).intern() : ""; - this.variant = (variant != null) ? variant.intern() : ""; + // This method must be called with normalize = false only when creating the + // Locale.* constants and non-normalized BaseLocale$Keys used for lookup. + private BaseLocale(String language, String script, String region, String variant, + boolean normalize) { + if (normalize) { + this.language = LocaleUtils.toLowerString(language).intern(); + this.script = LocaleUtils.toTitleString(script).intern(); + this.region = LocaleUtils.toUpperString(region).intern(); + this.variant = variant.intern(); + } else { + this.language = language; + this.script = script; + this.region = region; + this.variant = variant; + } } // Called for creating the Locale.* constants. No argument // validation is performed. public static BaseLocale createInstance(String language, String region) { - BaseLocale base = new BaseLocale(language, region); - CACHE.put(new Key(language, region), base); + BaseLocale base = new BaseLocale(language, "", region, "", false); + CACHE.put(new Key(base), base); return base; } @@ -84,7 +86,7 @@ } } - Key key = new Key(language, script, region, variant); + Key key = new Key(language, script, region, variant, false); BaseLocale baseLocale = CACHE.get(key); return baseLocale; } @@ -123,16 +125,16 @@ @Override public String toString() { StringJoiner sj = new StringJoiner(", "); - if (language.length() > 0) { + if (!language.isEmpty()) { sj.add("language=" + language); } - if (script.length() > 0) { + if (!script.isEmpty()) { sj.add("script=" + script); } - if (region.length() > 0) { + if (!region.isEmpty()) { sj.add("region=" + region); } - if (variant.length() > 0) { + if (!variant.isEmpty()) { sj.add("variant=" + variant); } return sj.toString(); @@ -155,10 +157,17 @@ } private static final class Key { - private final SoftReference lang; - private final SoftReference scrt; - private final SoftReference regn; - private final SoftReference vart; + /** + * Keep a SoftReference to the Key data if normalized (actually used + * as a cache key) and not initialized via the constant creation path. + * + * This allows us to avoid creating SoftReferences on lookup Keys + * (which are short-lived) and for Locales created via + * Locale#createConstant. + */ + private final SoftReference holderRef; + private final BaseLocale holder; + private final boolean normalized; private final int hash; @@ -166,15 +175,16 @@ * Creates a Key. language and region must be normalized * (intern'ed in the proper case). */ - private Key(String language, String region) { - assert language.intern() == language - && region.intern() == region; - - lang = new SoftReference<>(language); - scrt = new SoftReference<>(""); - regn = new SoftReference<>(region); - vart = new SoftReference<>(""); + private Key(BaseLocale locale) { + this.holder = locale; + this.holderRef = null; this.normalized = true; + String language = locale.getLanguage(); + String region = locale.getRegion(); + assert LocaleUtils.toLowerString(language).intern() == language + && LocaleUtils.toUpperString(region).intern() == region + && locale.getVariant() == "" + && locale.getScript() == ""; int h = language.hashCode(); if (region != "") { @@ -186,51 +196,64 @@ hash = h; } - public Key(String language, String script, String region, String variant) { - this(language, script, region, variant, false); + private Key(String language, String script, String region, + String variant, boolean normalize) { + if (language == null) { + language = ""; + } + if (script == null) { + script = ""; + } + if (region == null) { + region = ""; + } + if (variant == null) { + variant = ""; + } + + BaseLocale locale = new BaseLocale(language, script, region, variant, normalize); + this.normalized = normalize; + if (normalized) { + this.holderRef = new SoftReference<>(locale); + this.holder = null; + } else { + this.holderRef = null; + this.holder = locale; + } + this.hash = hashCode(locale); } - private Key(String language, String script, String region, - String variant, boolean normalized) { + public int hashCode() { + return hash; + } + + private int hashCode(BaseLocale locale) { int h = 0; - if (language != null) { - lang = new SoftReference<>(language); - int len = language.length(); - for (int i = 0; i < len; i++) { - h = 31*h + LocaleUtils.toLower(language.charAt(i)); - } - } else { - lang = new SoftReference<>(""); + String lang = locale.getLanguage(); + int len = lang.length(); + for (int i = 0; i < len; i++) { + h = 31*h + LocaleUtils.toLower(lang.charAt(i)); } - if (script != null) { - scrt = new SoftReference<>(script); - int len = script.length(); - for (int i = 0; i < len; i++) { - h = 31*h + LocaleUtils.toLower(script.charAt(i)); - } - } else { - scrt = new SoftReference<>(""); + String scrt = locale.getScript(); + len = scrt.length(); + for (int i = 0; i < len; i++) { + h = 31*h + LocaleUtils.toLower(scrt.charAt(i)); } - if (region != null) { - regn = new SoftReference<>(region); - int len = region.length(); - for (int i = 0; i < len; i++) { - h = 31*h + LocaleUtils.toLower(region.charAt(i)); - } - } else { - regn = new SoftReference<>(""); + String regn = locale.getRegion(); + len = regn.length(); + for (int i = 0; i < len; i++) { + h = 31*h + LocaleUtils.toLower(regn.charAt(i)); } - if (variant != null) { - vart = new SoftReference<>(variant); - int len = variant.length(); - for (int i = 0; i < len; i++) { - h = 31*h + variant.charAt(i); - } - } else { - vart = new SoftReference<>(""); + String vart = locale.getVariant(); + len = vart.length(); + for (int i = 0; i < len; i++) { + h = 31*h + vart.charAt(i); } - hash = h; - this.normalized = normalized; + return h; + } + + private BaseLocale getBaseLocale() { + return (holder == null) ? holderRef.get() : holder; } @Override @@ -238,46 +261,31 @@ if (this == obj) { return true; } - if (obj instanceof Key && this.hash == ((Key)obj).hash) { - String tl = this.lang.get(); - String ol = ((Key)obj).lang.get(); - if (tl != null && ol != null && - LocaleUtils.caseIgnoreMatch(ol, tl)) { - String ts = this.scrt.get(); - String os = ((Key)obj).scrt.get(); - if (ts != null && os != null && - LocaleUtils.caseIgnoreMatch(os, ts)) { - String tr = this.regn.get(); - String or = ((Key)obj).regn.get(); - if (tr != null && or != null && - LocaleUtils.caseIgnoreMatch(or, tr)) { - String tv = this.vart.get(); - String ov = ((Key)obj).vart.get(); - return (ov != null && ov.equals(tv)); - } - } + BaseLocale other = ((Key) obj).getBaseLocale(); + BaseLocale locale = this.getBaseLocale(); + if (other != null && locale != null + && LocaleUtils.caseIgnoreMatch(other.getLanguage(), locale.getLanguage()) + && LocaleUtils.caseIgnoreMatch(other.getScript(), locale.getScript()) + && LocaleUtils.caseIgnoreMatch(other.getRegion(), locale.getRegion()) + // variant is case sensitive in JDK! + && other.getVariant().equals(locale.getVariant())) { + return true; } } return false; } - @Override - public int hashCode() { - return hash; - } - public static Key normalize(Key key) { if (key.normalized) { return key; } - String lang = LocaleUtils.toLowerString(key.lang.get()).intern(); - String scrt = LocaleUtils.toTitleString(key.scrt.get()).intern(); - String regn = LocaleUtils.toUpperString(key.regn.get()).intern(); - String vart = key.vart.get().intern(); // preserve upper/lower cases - - return new Key(lang, scrt, regn, vart, true); + // Only normalized keys may be softly referencing the data holder + assert (key.holder != null && key.holderRef == null); + BaseLocale locale = key.holder; + return new Key(locale.getLanguage(), locale.getScript(), + locale.getRegion(), locale.getVariant(), true); } } @@ -288,18 +296,12 @@ @Override protected Key normalizeKey(Key key) { - assert key.lang.get() != null && - key.scrt.get() != null && - key.regn.get() != null && - key.vart.get() != null; - return Key.normalize(key); } @Override protected BaseLocale createObject(Key key) { - return new BaseLocale(key.lang.get(), key.scrt.get(), - key.regn.get(), key.vart.get()); + return Key.normalize(key).getBaseLocale(); } } } diff -r 85c27aabf312 -r 3d17a524da95 src/java.base/share/classes/sun/util/locale/LocaleObjectCache.java --- a/src/java.base/share/classes/sun/util/locale/LocaleObjectCache.java Thu Feb 08 10:06:57 2018 -0500 +++ b/src/java.base/share/classes/sun/util/locale/LocaleObjectCache.java Thu Feb 08 18:45:30 2018 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2018, 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 @@ -37,8 +37,8 @@ import java.util.concurrent.ConcurrentMap; public abstract class LocaleObjectCache { - private ConcurrentMap> map; - private ReferenceQueue queue = new ReferenceQueue<>(); + private final ConcurrentMap> map; + private final ReferenceQueue queue = new ReferenceQueue<>(); public LocaleObjectCache() { this(16, 0.75f, 16); @@ -57,17 +57,14 @@ value = entry.get(); } if (value == null) { + key = normalizeKey(key); V newVal = createObject(key); - // make sure key is normalized *after* the object creation - // so that newVal is assured to be created from a valid key. - key = normalizeKey(key); if (key == null || newVal == null) { // subclass must return non-null key/value object return null; } CacheEntry newEntry = new CacheEntry<>(key, newVal, queue); - entry = map.putIfAbsent(key, newEntry); if (entry == null) { value = newVal; @@ -92,7 +89,7 @@ private void cleanStaleEntries() { CacheEntry entry; while ((entry = (CacheEntry)queue.poll()) != null) { - map.remove(entry.getKey()); + map.remove(entry.getKey(), entry); } } diff -r 85c27aabf312 -r 3d17a524da95 test/jdk/java/util/Locale/SoftKeys.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/util/Locale/SoftKeys.java Thu Feb 08 18:45:30 2018 +0100 @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 + * @bug 8196869 + * @summary Make sure we deal with internal Key data being cleared properly + * @run main/othervm -Xms16m -Xmx16m -esa SoftKeys + */ +import java.util.*; + +public class SoftKeys { + + private static final char[] CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + public static void main(String[] args) { + // With 4 characters in "language", we'll fill up a 16M heap quickly, + // causing full GCs and SoftReference reclamation. Repeat at least two + // times to verify no NPEs appear when looking up Locale's whose + // softly referenced data in sun.util.locale.BaseLocale$Key might have + // been cleared. + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 512*1024; j++) { + new Locale(langForInt(j), "", ""); + } + } + } + + private static String langForInt(int val) { + StringBuilder buf = new StringBuilder(4); + buf.append(CHARS[(val >> 12) & 0xF]); + buf.append(CHARS[(val >> 8) & 0xF]); + buf.append(CHARS[(val >> 4) & 0xF]); + buf.append(CHARS[(val >> 0) & 0xF]); + return buf.toString(); + } +} +