Switch to internal implementation of StringInterner to avoid class loader
leaks when reloading the page in dev mode. (Not tuned.)
Review at http://gwt-code-reviews.appspot.com/1578804
Review by: tobyr@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10725 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/util/StringInterner.java b/dev/core/src/com/google/gwt/dev/util/StringInterner.java
index e193293..106e0e9 100644
--- a/dev/core/src/com/google/gwt/dev/util/StringInterner.java
+++ b/dev/core/src/com/google/gwt/dev/util/StringInterner.java
@@ -16,28 +16,73 @@
package com.google.gwt.dev.util;
-import com.google.gwt.thirdparty.guava.common.collect.Interner;
-import com.google.gwt.thirdparty.guava.common.collect.Interners;
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
/**
- * A utility class for reducing String memory waste. Note that this does not use
- * the String.intern() method which would prevent GC and fill the PermGen space.
- * Instead, we use a Google Collections WeakInterner.
+ * A utility class for reducing String memory waste.
+ *
+ * <p> We don't use the String.intern() method because it would prevent GC and fill the PermGen
+ * space. We also don't use Guava (for now) due to a class loader GC issue with the old
+ * version of Guava we're using. </p>
+ *
+ * <p> Thread-safe. The implementation uses shards to reduce thread contention. </p>
*/
public class StringInterner {
+ // chosen via performance testing with AWFE. (See AWFE load times speadsheet.)
+ private static int SHARD_BITS = 10;
+
+ static int SHARD_COUNT = 1 << SHARD_BITS;
+ private static int SHARD_MASK = SHARD_COUNT - 1;
+
private static final StringInterner instance = new StringInterner();
public static StringInterner get() {
return instance;
}
- private final Interner<String> stringPool = Interners.newWeakInterner();
+ private final Shard[] shards = new Shard[SHARD_COUNT];
protected StringInterner() {
+ for (int i = 0; i < SHARD_COUNT; i++) {
+ shards[i] = new Shard();
+ }
}
- public String intern(String s) {
- return stringPool.intern(s);
+ /**
+ * Returns a string equal to the input string, but not always the same.
+ */
+ public String intern(String original) {
+ int hashCode = original.hashCode();
+ int shardId = (hashCode ^ (hashCode >> SHARD_BITS)) & SHARD_MASK;
+ return shards[shardId].intern(original);
}
+ /**
+ * Returns the size of each shard, for testing shard balance.
+ */
+ int[] getShardSizes() {
+ int[] result = new int[SHARD_COUNT];
+ for (int i = 0; i < shards.length; i++) {
+ result[i] = shards[i].map.size();
+ }
+ return result;
+ }
+
+ private static class Shard {
+ private final WeakHashMap<String, WeakReference<String>> map =
+ new WeakHashMap<String, WeakReference<String>>();
+
+ private synchronized String intern(String original) {
+ WeakReference<String> ref = map.get(original);
+ String interned = ref == null ? null : ref.get();
+ if (interned == null) {
+ ref = new WeakReference<String>(original);
+ map.put(original, ref);
+ return original;
+ } else {
+ return interned;
+ }
+ }
+ }
}
diff --git a/dev/core/test/com/google/gwt/dev/util/StringInternerTest.java b/dev/core/test/com/google/gwt/dev/util/StringInternerTest.java
new file mode 100644
index 0000000..c17104a
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/util/StringInternerTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.util;
+
+import junit.framework.TestCase;
+
+/**
+ * Verifies that we can intern strings.
+ *
+ * @author skybrian@google.com (Brian Slesinsky)
+ */
+public class StringInternerTest extends TestCase {
+ private StringInterner interner;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ interner = StringInterner.get();
+ }
+
+ public void testCanInternString() throws Exception {
+ String firstIn = "fnord123";
+ String secondIn = new String(firstIn);
+ assertFalse(firstIn == secondIn);
+
+ String firstOut = interner.intern(firstIn);
+ String secondOut = interner.intern(secondIn);
+ assertSame(firstOut, secondOut);
+ }
+
+ public void testCanFreeString() throws Exception {
+ // Intern about a gigabyte of data.
+ // If we don't free any interned strings, we should run out of memory.
+ // (Verified that it fails using Interns.newStrongInterner().)
+ String prefix = repeat('a', 1000);
+ for (int i = 0; i < 1000 * 1000; i++) {
+ interner.intern(prefix + i);
+ }
+ }
+
+ public void testShardsAreSomewhatBalanced() throws Exception {
+ for (int i = 0; i < 1000000; i++) {
+ interner.intern("foo" + i);
+ }
+
+ int[] shardSizes = interner.getShardSizes();
+ assertEquals(StringInterner.SHARD_COUNT, shardSizes.length);
+
+ int minShardSize = Integer.MAX_VALUE;
+ int maxShardSize = 0;
+ int total = 0;
+ for (int candidate : shardSizes) {
+ minShardSize = Math.min(minShardSize, candidate);
+ maxShardSize = Math.max(maxShardSize, candidate);
+ total += candidate;
+ }
+
+ int meanShardSize = total / shardSizes.length;
+ int expectedMinShardSize = meanShardSize / 5;
+ int expectedMaxShardSize = meanShardSize * 5;
+ if (minShardSize < expectedMinShardSize || maxShardSize > expectedMaxShardSize) {
+ fail("unbalanced shards: [" + minShardSize + "," + maxShardSize + "] not within [" +
+ expectedMinShardSize + ", " + expectedMaxShardSize + "]");
+ }
+ }
+
+ private static String repeat(char c, int length) {
+ StringBuilder buffer = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ buffer.append(c);
+ }
+ return buffer.toString();
+ }
+}