java6,7,8中字符串池的进化史与深度剖析

这篇文章将要讨论 Java 6 中是如何实现 String.intern 方法的,以及这个方法在 Java 7 以及 Java 8 中做了哪些调整。

字符串池

字符串池(有名字符串标准化)是通过使用唯一的共享 String 对象来使用相同的值不同的地址表示字符串的过程。你可以使用自己定义的 Map<String,
String>
 (根据需要使用 weak 引用或者 soft 引用)并使用 map 中的值作为标准值来实现这个目标,或者你也可以使用 JDK 提供的 String.intern()

很多标准禁止在 Java 6 中使用 String.intern() 因为如果频繁使用池会失去控制,有很大的几率触发 OutOfMemoryException。Oracle
Java 7 对字符串池做了很多改进,你可以通过以下地址进行了解 http://bugs.sun.com/view_bug.do?bug_id=6962931以及 http://bugs.sun.com/view_bug.do?bug_id=6962930

Java 6 中的 String.intern()

在美好的过去所有共享的 String 对象都存储在 PermGen 中 — 堆中固定大小的部分主要用于存储加载的类对象和字符串池。除了明确的共享字符串,PermGen 字符串池还包含所有程序中使用过的字符串(这里要注意是使用过的字符串,如果类或者方法从未加载或者被条用,在其中定义的任何常量都不会被加载)

Java 6 中字符串池的最大问题是它的位置 — PermGen。PermGen 的大小是固定的并且在运行时是无法扩展的。你可以使用 -XX:MaxPermSize=N 配置来调整它的大小。据我了解,对于不同的平台默认的
PermGen 大小在 32M 到 96M 之间。你可以扩展它的大小,不过大小使用都是固定的。这个限制需要你在使用 String.intern 时需要非常小心 — 你最好不要使用这个方法 intern
任何无法控制的用户输入。这是为什么在 JAVA6 中大部分使用手动管理 Map 来实现字符串池

Java 7 中的 String.intern()

Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变 — 字符串池的位置被调整到 heap 中了。这意味着你再也不会被固定的内存空间限制了。所有的字符串都保存在堆(heap)中同其他普通对象一样,这使得你在调优应用时仅需要调整堆大小。这 个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用 String.intern()。

字符串池中的数据会被垃圾收集

没错,在 JVM 字符串池中的所有字符串会被垃圾收集,如果这些值在应用中没有任何引用。这是用于所有版本的 Java,这意味着如果 interned
的字符串在作用域外并且没有任何引用 — 它将会从 JVM 的字符串池中被垃圾收集掉。

因为被重新定位到堆中以及会被垃圾收集,JVM 的字符串池看上去是存放字符串的合适位置,是吗?理论上是 — 违背使用的字符串会从池中收集掉,当外部输入一个字符传且池中存在时可以节省内存。看起来是一个完美的节省内存的策略?在你回答这个之前,可以肯定的是你 需要知道字符串池是如何实现的。

在 Java 6,7,8 中 JVM 字符串池的实现

字符串池是使用一个拥有固定容量的 HashMap 每个元素包含具有相同 hash 值的字符串列表。一些实现的细节可以从 Java bug 报告中获得 http://bugs.sun.com/view_bug.do?bug_id=6962930

默认的池大小是 1009 (出现在上面提及的 bug 报告的源码中,在
Java7u40 中增加了)。在 JAVA 6 早期版本中是一个常量,在随后的 java6u30
至 java6u41 中调整为可配置的。而在java 7中一开始就是可以配置的(至少在java7u02中是可以配置的)。你需要指定参数 -XX:StringTableSize=N,  N 是字符串池 Map 的大小。确保它是为性能调优而预先准备的大小。

在 Java 6 中这个参数没有太多帮助,因为你仍任被限制在固定的 PermGen 内存大小中。后续的讨论将直接忽略 Java 6

Java 7 (直至 Java7u40)

在 Java7 中,换句话说,你被限制在一个更大的堆内存中。这意味着你可以预先设置好 String 池的大小(这个值取决于你的应用程序需求)。通常说来,一旦程序开始内存消耗,内存都是成百兆的增长,在这种情况下,给一个拥有 100 万字符串对象的字符串池分配 8-16M 的内存看起来是比较适合的(不要使用1,000,000 作为 -XX:StringTaleSize 的值
– 它不是质数;使用 1,000,003代替)

你可能期待关于 String 在 Map 中的分配 — 可以阅读我之前关于 HashCode 方法调优的经验。

你必须设置一个更大的 -XX:StringTalbeSize 值(相比较默认的
1009 ),如果你希望更多的使用 String.intern()
— 否则这个方法将很快递减到 0 (池大小)。

我没有注意到在 intern 小于 100 字符的字符串时的依赖情况(我认为在一个包含 50 个重复字符的字符串与现实数据并不相似,因此 100 个字符看上去是一个很好的测试限制)

下面是默认池大小的应用程序日志:第一列是已经 intern 的字符串数量,第二列 intern 10,000 个字符串所有的时间(秒)


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

0;
time =
0.0sec

50000;
time =
0.03sec

100000;
time =
0.073sec

150000;
time =
0.13sec

200000;
time =
0.196sec

250000;
time =
0.279sec

300000;
time =
0.376sec

350000;
time =
0.471sec

400000;
time =
0.574sec

450000;
time =
0.666sec

500000;
time =
0.755sec

550000;
time =
0.854sec

600000;
time =
0.916sec

650000;
time =
1.006sec

700000;
time =
1.095sec

750000;
time =
1.273sec

800000;
time =
1.248sec

850000;
time =
1.446sec

900000;
time =
1.585sec

950000;
time =
1.635sec

1000000;
time =
1.913sec

测试是在 Core [email protected] CPU 设备上进行的。你可以看到,它成线性增长,并且在 JVM 字符串池包含一百万个字符串时,我仍然可以近似每秒 intern 5000
个字符串,这对于在内存中处理大量数据的应用程序来说太慢了。

现在,调整 -XX:StringTableSize=100003 参数来重新运行测试:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

50000;
time =
0.017sec

100000;
time =
0.009sec

150000;
time =
0.01sec

200000;
time =
0.009sec

250000;
time =
0.007sec

300000;
time =
0.008sec

350000;
time =
0.009sec

400000;
time =
0.009sec

450000;
time =
0.01sec

500000;
time =
0.013sec

550000;
time =
0.011sec

600000;
time =
0.012sec

650000;
time =
0.015sec

700000;
time =
0.015sec

750000;
time =
0.01sec

800000;
time =
0.01sec

850000;
time =
0.011sec

900000;
time =
0.011sec

950000;
time =
0.012sec

1000000;
time =
0.012sec

可以看到,这时插入字符串的时间近似于常量(在 Map 的字符串列表中平均字符串个数不超过 10 个),下面是相同设置的结果,不过这次我们将向池中插入 1000 万个字符串(这意味着 Map 中的字符串列表平均包含 100 个字符串)


1

2

3

4

5

6

7

8

9

2000000;
time =
0.024sec

3000000;
time =
0.028sec

4000000;
time =
0.053sec

5000000;
time =
0.051sec

6000000;
time =
0.034sec

7000000;
time =
0.041sec

8000000;
time =
0.089sec

9000000;
time =
0.111sec

10000000;
time =
0.123sec

现在让我们将池的大小增加到 100 万(精确的说是 1,000,003)


1

2

3

4

5

6

7

8

9

10

1000000;
time =
0.005sec

2000000;
time =
0.005sec

3000000;
time =
0.005sec

4000000;
time =
0.004sec

5000000;
time =
0.004sec

6000000;
time =
0.009sec

7000000;
time =
0.01sec

8000000;
time =
0.009sec

9000000;
time =
0.009sec

10000000;
time =
0.009sec

如你所看到的,时间非常平均,并且与 “0 到 100万” 的表没有太大差别。甚至在池大小足够大的情况下,我的笔记本也能每秒添加1,000,000个字符对象。

我们还需要手工管理字符串池吗?

现在我们需要对比 JVM 字符串池和 WeakHashMap<String, WeakReference<String>> 它可以用来模拟 JVM 字符串池。下面的方法用来替换 String.intern


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

privatestatic

final

WeakHashMap<String, WeakReference<String>> s_manualCache =

    newWeakHashMap<String,
WeakReference<String>>(
100000);

privatestatic

String manualIntern(
finalString
str )

{

    finalWeakReference<String>
cached = s_manualCache.get( str );

    if(
cached !=
null)

    {

        finalString
value = cached.get();

        if(
value !=
null)

            returnvalue;

    }

    s_manualCache.put(
str,
newWeakReference<String>(
str ) );

    returnstr;

}

下面针对手工池的相同测试:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

0;
manual time =
0.001sec

50000;
manual time =
0.03sec

100000;
manual time =
0.034sec

150000;
manual time =
0.008sec

200000;
manual time =
0.019sec

250000;
manual time =
0.011sec

300000;
manual time =
0.011sec

350000;
manual time =
0.008sec

400000;
manual time =
0.027sec

450000;
manual time =
0.008sec

500000;
manual time =
0.009sec

550000;
manual time =
0.008sec

600000;
manual time =
0.008sec

650000;
manual time =
0.008sec

700000;
manual time =
0.008sec

750000;
manual time =
0.011sec

800000;
manual time =
0.007sec

850000;
manual time =
0.008sec

900000;
manual time =
0.008sec

950000;
manual time =
0.008sec

1000000;
manual time =
0.008sec

当 JVM 有足够内存时,手工编写的池提供了良好的性能。不过不幸的是,我的测试(保留 String.valueOf(0 < N < 1,000,000,000))保留非常短的字符串,在使用 -Xmx1280M 参数时它允许我保留月为
2.5M 的这类字符串。JVM 字符串池 (size=1,000,003)从另一方面讲在 JVM 内存足够时提供了相同的性能特性,知道 JVM 字符串池包含 12.72M 的字符串并消耗掉所有内存(5倍多)。我认为,这非常值得你在你的应用中去掉所有手工字符串池。

在 Java 7u40+ 以及 Java 8 中的 String.intern()

Java7u40 版本扩展了字符串池的大小(这是组要的性能更新)到 60013.这个值允许你在池中包含大约 30000 个独立的字符串。通常来说,这对于需要保存的数据来说已经足够了,你可以通过 -XX:+PrintFlagsFinal JVM
参数获得这个值。

我尝试在原始发布的 Java 8 中运行相同的测试,Java 8 仍然支持 -XX:StringTableSize 参数来兼容 Java 7 特性。主要的区别在于 Java 8 中默认的池大小增加到
60013:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

50000;
time =
0.019sec

100000;
time =
0.009sec

150000;
time =
0.009sec

200000;
time =
0.009sec

250000;
time =
0.009sec

300000;
time =
0.009sec

350000;
time =
0.011sec

400000;
time =
0.012sec

450000;
time =
0.01sec

500000;
time =
0.013sec

550000;
time =
0.013sec

600000;
time =
0.014sec

650000;
time =
0.018sec

700000;
time =
0.015sec

750000;
time =
0.029sec

800000;
time =
0.018sec

850000;
time =
0.02sec

900000;
time =
0.017sec

950000;
time =
0.018sec

1000000;
time =
0.021sec

测试代码

这篇文章的测试代码很简单,一个方法中循环创建并保留新字符串。你可以测量它保留 10000 个字符串所需要的时间。最好配合 -verbose:gc JVM 参数来运行这个测试,这样可以查看垃圾收集是何时以及如何发生的。另外最好使用 -Xmx 参数来执行堆的最大值。

这里有两个测试:testStringPoolGarbageCollection 将显示 JVM 字符串池被垃圾收集 — 检查垃圾收集日志消息。在 Java 6 的默认 PermGen 大小配置上,这个测试会失败,因此最好增加这个值,或者更新测试方法,或者使用
Java 7.

第二个测试显示内存中保留了多少字符串。在 Java 6 中执行需要两个不同的内存配置 比如: -Xmx128M 以及 -Xmx1280M (10
倍以上)。你可能发现这个值不会影响放入池中字符串的数量。另一方面,在 Java 7 中你能够在堆中填满你的字符串。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

/**

 -
Testing String.intern.

 *

 -
Run this class at least with -verbose:gc JVM parameter.

 */

publicclass

InternTest {

    publicstatic

void

main( String[] args ) {

        testStringPoolGarbageCollection();

        testLongLoop();

    }

    /**

     -
Use this method to see where interned strings are stored

     -
and how many of them can you fit for the given heap size.

     */

    privatestatic

void

testLongLoop()

    {

        test(1000*
1000*
1000);

        //uncomment
the following line to see the hand-written cache performance

        //testManual(
1000 * 1000 * 1000 );

    }

    /**

     -
Use this method to check that not used interned strings are garbage collected.

     */

    privatestatic

void

testStringPoolGarbageCollection()

    {

        //first
method call - use it as a reference

        test(1000*
1000);

        //we
are going to clean the cache here.

        System.gc();

        //check
the memory consumption and how long does it take to intern strings

        //in
the second method call.

        test(1000*
1000);

    }

    privatestatic

void

test(
finalint

cnt )

    {

        finalList<String>
lst =
newArrayList<String>(
100);

        longstart
= System.currentTimeMillis();

        for(
inti
=
0;
i < cnt; ++i )

        {

            finalString
str =
"Very
long test string, which tells you about something "

+

            "very-very
important, definitely deserving to be interned #"

+ i;

//uncomment
the following line to test dependency from string length

//           
final String str = Integer.toString( i );

            lst.add(
str.intern() );

            if(
i %
10000==
0)

            {

                System.out.println(
i +
";
time = "

+ ( System.currentTimeMillis() - start ) /
1000.0+
"
sec"

);

                start
= System.currentTimeMillis();

            }

        }

        System.out.println("Total
length = "

+ lst.size() );

    }

    privatestatic

final

WeakHashMap<String, WeakReference<String>> s_manualCache =

        newWeakHashMap<String,
WeakReference<String>>(
100000);

    privatestatic

String manualIntern(
finalString
str )

    {

        finalWeakReference<String>
cached = s_manualCache.get( str );

        if(
cached !=
null)

        {

            finalString
value = cached.get();

            if(
value !=
null)

                returnvalue;

        }

        s_manualCache.put(
str,
newWeakReference<String>(
str ) );

        returnstr;

    }

    privatestatic

void

testManual(
finalint

cnt )

    {

        finalList<String>
lst =
newArrayList<String>(
100);

        longstart
= System.currentTimeMillis();

        for(
inti
=
0;
i < cnt; ++i )

        {

            finalString
str =
"Very
long test string, which tells you about something "

+

                "very-very
important, definitely deserving to be interned #"

+ i;

            lst.add(
manualIntern( str ) );

            if(
i %
10000==
0)

            {

                System.out.println(
i +
";
manual time = "

+ ( System.currentTimeMillis() - start ) /
1000.0+
"
sec"

);

                start
= System.currentTimeMillis();

            }

        }

        System.out.println("Total
length = "

+ lst.size() );

    }

}

总结

  • 由于 Java 6 中使用固定的内存大小(PermGen)因此不要使用 String.intern() 方法
  • Java7 和 8 在堆内存中实现字符串池。这以为这字符串池的内存限制等于应用程序的内存限制。
  • 在 Java 7 和 8 中使用 -XX:StringTableSize 来设置字符串池 Map 的大小。它是固定的,因为它使用 HashMap 实现。近似于你应用单独的字符串个数(你希望保留的)并且设置池的大小为最接近的质数并乘以
    2 (减少碰撞的可能性)。它是的 String.intern 可以使用相同(固定)的时间并且在每次插入时消耗更小的内存(同样的任务,使用java WeakHashMap将消耗4-5倍的内存)。
  • 在 Java 6 和 7(Java7u40以前) 中 -XX:StringTableSize 参数的值是 1009。Java7u40 以后这个值调整为 60013 (Java 8 中使用相同的值)
  • 如果你不确定字符串池的用量,参考:-XX:+PrintStringTableStatistics JVM 参数,当你的应用挂掉时它告诉你字符串池的使用量信息。

原文:http://java-performance.info/string-intern-in-java-6-7-8/

译文:http://www.4byte.cn/learning/84930/java-xing-neng-you-hua-shou-ce-ti-gao-java-dai-ma-xing-neng-de-ge-zhong-ji-qiao.html

时间: 2024-09-30 18:57:18

java6,7,8中字符串池的进化史与深度剖析的相关文章

java中字符串池,String池,共享池到底是怎么回事?

栈中有共享池的概念,(视频下载) (全部书籍)比如下面例子中,sz="hello";在栈中创建一个String对象引用变量sz,然后看看栈中有没有"hello",如果没有,则将"hello"存放进栈,并令sz指向"hello",如果已经有"hello" 则直接令sz指向"hello".对于int, float 类型的变量也是一样的有这种共享池的概念, 对于下面程序中:ss0 = new

C#中字符串的内存分配与驻留池

刚开始学习C#的时候,就听说CLR对于String类有一种特别的内存管理机制:有时候,明明声明了两个String类的对象,但是他们偏偏却指向同一个实例.如下: String s1 = "Hello";String s2 = "Hello"; //s2和s1的实际值都是"Hello"bool same = (object) s1 == (object) s2; //这里比较s1.s2是否引用了同一个对象实例 //所以不能写作bool same =

【基础】Java 8 中的常量池、字符串池、包装类对象池

1 - 引言 2 - 常量池 2.1 你真的懂 Java的“字面量”和“常量”吗? 2.2 常量和静态/运行时常量池有什么关系?什么是常量池? 2.3 字节码下的常量池以及常量池的加载机制 2.4 是不是所有的数字字面量都会被存到常量池中?3 - 包装类对象池 $\ne$JVM 常量池4 - 字符串池 4.1 字符串池的实现——StringTable 4.2 字符串池存的是实例还是引用?5 - 补充 5.1 永久代为何被 HotSpot VM 废弃? 5.2 为什么 Java 要分常量.简单类型

Java内存管理-探索Java中字符串String(十二)

做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 一.初识String类 首先JDK API的介绍: public final class String extends Object implements Serializable, Comparable<String>, CharSequence String类代表字符串.Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现. 字符串是常量:它们的值在创建之后不能更改.字符

java中字符串常量,堆栈的区别和字符串函数intern()

转自:http://txy821.iteye.com/blog/760957 java.lang.String的intern()方法 "abc".intern()方法的返回值还是字符串"abc",表面上看起来好像这个方法没什么用处.但实际上,它做了个小动作: 检查字符串池里是否存在"abc"这么一个字符串,如果存在,就返回池里的字符串:如果不存在,该方法会把"abc"添加到字符串池中,然后再返回它的引用. 我们做个测试: Ja

Java中字符串比较时==和equals的区别

==是比较两个字符串引用的地址是否相同,即是否指向同一个对象,而equals方法则比较字符串的内容是否相同. 例如String a = "abc"; String b = "abc"; a == b返回true,a.equals(b)同样返回true,这是为什么呢? 原来程序在运行时有一个字符串池,创建字符串时会先查找池中是否有相应的字符串,如果已经存在的话只需把引用指向它即可,如果没有则新建一个. 上例中创建a时,会在字符串池中首先创建一个"abc&qu

Java中字符串对象

Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池. 工作原理 当代码中出现字

字符串池

String a = "Hello"; String b = "Hello"; String c = new String("Hello"); String d = new String("Hello"); System.out.println(a == b); System.out.println(b == c); System.out.println(c == d); System.out.println(a.equals

日常总结 -- 字符串池

最近被人问到关于java字符串的问题,做了一点小的归纳先上经典代码: 1.String a = "Hello"; 2.String b = "Hello"; 3.String c = new String("Hello"); 4.String d = new String("Hello"); System.out.println(a == b); System.out.println(b == c); System.out.p