8196869: Optimize Locale creation
authorredestad
Thu, 08 Feb 2018 18:45:30 +0100
changeset 48763 3d17a524da95
parent 48762 85c27aabf312
child 48764 76ebfaa3cc3f
child 48768 ac007e4bbf4b
8196869: Optimize Locale creation Reviewed-by: psandoz, plevart, naoto
src/java.base/share/classes/java/util/Locale.java
src/java.base/share/classes/sun/util/locale/BaseLocale.java
src/java.base/share/classes/sun/util/locale/LocaleObjectCache.java
test/jdk/java/util/Locale/SoftKeys.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<LocaleKey, Locale> {
+    private static class Cache extends LocaleObjectCache<Object, Locale> {
         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);
+            }
         }
     }
 
--- 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<String> lang;
-        private final SoftReference<String> scrt;
-        private final SoftReference<String> regn;
-        private final SoftReference<String> 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<BaseLocale> 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();
         }
     }
 }
--- 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<K, V> {
-    private ConcurrentMap<K, CacheEntry<K, V>> map;
-    private ReferenceQueue<V> queue = new ReferenceQueue<>();
+    private final ConcurrentMap<K, CacheEntry<K, V>> map;
+    private final ReferenceQueue<V> 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<K, V> newEntry = new CacheEntry<>(key, newVal, queue);
-
             entry = map.putIfAbsent(key, newEntry);
             if (entry == null) {
                 value = newVal;
@@ -92,7 +89,7 @@
     private void cleanStaleEntries() {
         CacheEntry<K, V> entry;
         while ((entry = (CacheEntry<K, V>)queue.poll()) != null) {
-            map.remove(entry.getKey());
+            map.remove(entry.getKey(), entry);
         }
     }
 
--- /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();
+    }
+}
+