string比大小_你,确定了解Java的String字符串?

本文将描述JDK6中String.intern()是如何实现的,以及在JDK7和JDK8中对字符串池化技术做了哪些改变。

String池化介绍

String池化就是把一些值相同,但是标识符不同的字符串用一个共享的String对象来表示,而不是用多个String对象。举个栗子,如下代码所示,3个变量的值完全一样,那么通过String池化处理,3个变量a,b,c事实上指向同一个String对象:

String a = "afei";

String b = "afei";

String c = "afei";

要达到这个目的,你可以自己借助Map实现,这就是传说中的重复造轮子。当然,最优雅的方式还是利用JDK提供的String.intern()方法。

在JDK6的时候,很多标准规范禁止使用String.intern(),因为如果池化控制不好的话,非常可能碰到OOM。因此,JDK7做了相当大的改变,更多细节可以参考下面给到的两个链接地址:

http://bugs.sun.com/viewbug.do?bugid=6962931

http://bugs.sun.com/viewbug.do?bugid=6962930

JDK6中的String.intern()

JDK6时代,所有通过String.intern()处理过的字符串都被保存在PermGen中,是Java堆的一个固定大小部分,主要用来存储已经加载的类信息和字符串常量池。除了显示调用String.intern()的字符串,PermGen的字符串常量池也包含了程序中所有使用过的字符串(如果一个类或者方法从来没有被加载,或者被调用,那就不是使用过的)。

JDK6中字符串常量池这样的实现最大的问题的就是它保存的地方--PermGen。PermGen的大小是固定的,不能在运行时扩容,用户可以通过JVM参数-XX:MaxPermSize=N设定PermGen的大小。这样的限制导致我们在使用String.intern()时需要非常小心,如果你想在JDK6中使用String.intern(),那么你最好不要使用在不能被控制的的地方(例如池化用户名,这绝对是灾难)。

JDK7中的String.intern()

Oracle工程师在JDK7中对字符串常量池逻辑做了非常重要的改变 -- 将字符串常量池重新移到了堆中,它不再是一块分离的固定大小的内存区域。所有字符串都被移到堆中,和其他大多数对象一样。如此一来,当你调优应用时只需要管理堆尺寸大小。如果是基于JDK7的应用,那么,这绝对是一个充分的理由让你重新考虑使用String.intern(),当然也有一些其他的理由。

池化的值能被GC

是的,JVM字符串常量池中所有的字符串都能被垃圾回收掉,前提条件是那些不再被GC Roots引用的字符串 -- 这个结论适用于我们正在讨论的JDK6,7,8三个版本。这就意味着,如果你通过String.intern()池化的字符串超过了范围,并且不再被引用,那么它们是可以被GC掉的。

字符串常量池中的字符串能够被GC,也能保留在Java堆中。这样看来,对所有字符串来说,字符串常量池似乎是一个很合适的地方,理论上是这样的 -- 没有使用的字符串会在常量池中被GC掉,使用的字符串允许你保存在内存中,这样当然取等值的字符串时就能直接从常量池中取出来。听起来似乎是一个完美的保存策略,但是在下结论之前,你需要先了解字符串常量池实现的细节。

JDK6/7/8中字符串常量池的实现

字符串常量池被实现为一个固定容量大小的HashTable,每个桶(bucket)包含所有hashcode值相同的字符串组成的List链表。默认池大小是1009,JDK6早期版本是固定常量池,JDK6u30~6u41是可以配置的,JDK7从7u02开始也是支持配置的,通过JVM参数-XX:StringTableSize=N即可指定。需要提醒的是,为了更好的性能和散列性,建议N为一个质数(prime number,例如13,17,1009这样的数)。

JDK6中StringTableSize这个参数帮助价值不大,因为还是受到PermGen固定大小的限制。所以,接下来的讨论将抛弃JDK6。

另外,需要注意的是,字符串常量池这个HashTable,当存放的数据足够多需要rehash时,其rehash的行为和Java中使用的HashMap有所不同。字符串常量池底层的HashTable在rehash时不会扩容,即rehash前后bucket数量是一样的。它的rehash过程只是通过一个新的seed重新计算一遍来尝试摊平每个bucket中LinkedList的长度(不保证rehash后的性能有很大的提升)。

Java7 (until Java7u40)

需要说明的是,这个段落只表示JDK7u40之前的JDK7版本。

JDK7中,一方面你将受到更大堆内存的限制,意味着你能设置一个更大的字符串常量池(具体多大取决于你应用需求)。通常来说,当内存数据尺寸增长几百M后,我们才会开始关心内存消耗,在这种场景下,为常量池分配8~16M用来保存百万级个entry似乎是一个很合理的权衡(注意:不要设置-XX:StringTableSize的值为1000000,而应该设置为100003,因为1000000不是质数)。

如果你真的想要使用String.intern(),强烈建议将-XX:StringTableSize设置一个比默认值1009更大的值。否则,这个方法的性能很快就会退化到取决于双向链表长度。

以下是使用默认池大小的部分应用测试日志:在已经缓存了一些字符串时存入10000个字符串花费的时间,这1w个字符串通过Integer.toString(i)生成,其中i在[0, 1000000)之间:

0; time = 0.0 sec

50000; time = 0.030 sec

100000; time = 0.073 sec

150000; time = 0.130 sec

200000; time = 0.196 sec

250000; time = 0.279 sec

300000; time = 0.376 sec

350000; time = 0.471 sec

400000; time = 0.574 sec

450000; time = 0.666 sec

500000; time = 0.755 sec

550000; time = 0.854 sec

600000; time = 0.916 sec

650000; time = 1.006 sec

700000; time = 1.095 sec

750000; time = 1.273 sec

800000; time = 1.248 sec

850000; time = 1.446 sec

900000; time = 1.585 sec

950000; time = 1.635 sec

1000000; time = 1.913 sec

这个测试结果基于Core [email protected] CPU。正如你看到的结果,耗时线性增长,当JVM常量池已经有1百万个字符串时,接下来每秒钟只能池化大概5000个字符串,这时候性能衰退还是很严重的。但是,当我们设置-XX:StringTableSize=100003后,再次进行测试,结果如下。正如你看到的,这种情况下,池化字符串的耗时比较稳定(每个bucket平均有10个字符串):

50000; time = 0.017 sec

100000; time = 0.009 sec

150000; time = 0.010 sec

200000; time = 0.009 sec

250000; time = 0.007 sec

300000; time = 0.008 sec

350000; time = 0.009 sec

400000; time = 0.009 sec

450000; time = 0.010 sec

500000; time = 0.013 sec

550000; time = 0.011 sec

600000; time = 0.012 sec

650000; time = 0.015 sec

700000; time = 0.015 sec

750000; time = 0.010 sec

800000; time = 0.010 sec

850000; time = 0.011 sec

900000; time = 0.011 sec

950000; time = 0.012 sec

1000000; time = 0.012 sec

接下来,我们依然设置-XX:StringTableSize=100003,但是准备池化1千万个字符串,这就意味着平均每个bucket中大概有100个字符串:

2000000; time = 0.024 sec

3000000; time = 0.028 sec

4000000; time = 0.053 sec

5000000; time = 0.051 sec

6000000; time = 0.034 sec

7000000; time = 0.041 sec

8000000; time = 0.089 sec

9000000; time = 0.111 sec

10000000; time = 0.123 sec

接下来,我们设置-XX:StringTableSize=10000003,并插入1千万个字符串继续进行测试。结果如下,正如你看到的,耗时是水平的而不是线性增长。由此可见,只要常量池大小足够大,即使是我这种个人电脑也能轻轻松松达到每秒百万级的插入量。

1000000; time = 0.005 sec

2000000; time = 0.005 sec

3000000; time = 0.005 sec

4000000; time = 0.004 sec

5000000; time = 0.004 sec

6000000; time = 0.009 sec

7000000; time = 0.010 sec

8000000; time = 0.009 sec

9000000; time = 0.009 sec

10000000; time = 0.009 sec

还需要重复造轮子嘛?

现在,我们比较一下JVM自带的字符串常量池和WeakHashMap

private static final WeakHashMap> s_manualCache = new WeakHashMap>( 100000 );

private static String manualIntern(final String str){

final WeakReference cached = s_manualCache.get(str);

if (cached != null){

final String value = cached.get();

if (value != null){

return value;

}

}

s_manualCache.put(str, new WeakReference(str));

return str;

}

利用WeakHashMap造出的轮子测试性能如下:

0; manual time = 0.001 sec

50000; manual time = 0.03 sec

100000; manual time = 0.034 sec

150000; manual time = 0.008 sec

200000; manual time = 0.019 sec

250000; manual time = 0.011 sec

300000; manual time = 0.011 sec

350000; manual time = 0.008 sec

400000; manual time = 0.027 sec

450000; manual time = 0.008 sec

500000; manual time = 0.009 sec

550000; manual time = 0.008 sec

600000; manual time = 0.008 sec

650000; manual time = 0.008 sec

700000; manual time = 0.008 sec

750000; manual time = 0.011 sec

800000; manual time = 0.007 sec

850000; manual time = 0.008 sec

900000; manual time = 0.008 sec

950000; manual time = 0.008 sec

1000000; manual time = 0.008 sec

当JVM内存充足时,利用WeakHashMap造的轮子和JVM内置常量池性能相当。但是,在我缓存短字符串进行字符串缓存测试时,Xmx1280M的设置只能缓存约2.5M的strings,但是JVM常量池在保证相同性能的情况下可以缓存约12.72M的strings(5倍多)。因此我认为,我们最好在程序中避免自己实现字符串常量池。

7u40+和8中的String.intern()

7u40以后的版本字符串常量池大小被增大到60013(60013也是质数)。默认值改为60013可以让你保存大概30000个不同的字符串到常量池中而不会发生任何Hash碰撞。通常来说,这个值完全足够你保存那些真正需要池化的字符串。这个默认值你可以通过如下命令进行查看,通过结果我们可知,JDK8默认值就是60013:

afei$ java -XX:+PrintFlagsFinal -version | grep StringTableSize uintx StringTableSize = 60013 {product} java version "1.8.0201" Java(TM) SE Runtime Environment (build 1.8.0201-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode) 接下来我尝试在JDK8环境上运行相同的测试用例,Java8依旧支持-XX:StringTableSize参数且提供了与Java7同样的性能表现。只是唯一的不同在于默认池大小增长为60013了:

50000; time = 0.019 sec

100000; time = 0.009 sec

150000; time = 0.009 sec

200000; time = 0.009 sec

250000; time = 0.009 sec

300000; time = 0.009 sec

350000; time = 0.011 sec

400000; time = 0.012 sec

450000; time = 0.010 sec

500000; time = 0.013 sec

550000; time = 0.013 sec

600000; time = 0.014 sec

650000; time = 0.018 sec

700000; time = 0.015 sec

750000; time = 0.029 sec

800000; time = 0.018 sec

850000; time = 0.020 sec

900000; time = 0.017 sec

950000; time = 0.018 sec

1000000; time = 0.021 sec

测试代码

这篇文章中使用的测试代码非常简单:在循环中不断生成和池化新的strings。我们同时计算了它池化当前10000个字符串的耗时。运行此程序时,强烈建议用-verbose:gc这个虚拟机参数,以便当发生GC时,能在控制台实时输出GC日志。你也可以使用-Xmx参数来指定最大堆空间。

这里有2个测试:testStringPoolGarbageCollection 测试将会证明JVM字符串常量池真的可以被垃圾回收 —— 查看垃圾回收日志并在随后查看缓存字符串的耗时。这个测试在Java6中默认的永久代区大小中会失败。因此要么更新大小,要么更新测试方法参数,要么使用Java7。 第二个测试将会向你展示内存中可以缓存多少字符串。请在Java6中通过两个不同的内存设定运行此测试。例如-Xmx128M和-Xmx1280M(后者是前者的10倍)。你会发现这并不会影响可在池中缓存字符串的数目,因为那块保存池化的字符串空间大小是固定的。而在Java7中,你却可以用你的字符串填满整个堆。

/**

* Testing String.intern.

*

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

*/

public class InternTest {

public static 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.

*/

private static 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.

*/

private static 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 );

}

private static void test( final int cnt )

{

final List lst = new ArrayList( 100 );

long start = System.currentTimeMillis();

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

{

final String 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() );

}

private static final WeakHashMap> s_manualCache =

new WeakHashMap>( 100000 );

private static String manualIntern( final String str )

{

final WeakReference cached = s_manualCache.get( str );

if ( cached != null )

{

final String value = cached.get();

if ( value != null )

return value;

}

s_manualCache.put( str, new WeakReference( str ) );

return str;

}

private static void testManual( final int cnt )

{

final List lst = new ArrayList( 100 );

long start = System.currentTimeMillis();

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

{

final String 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() );

}

}

总结

接下来对String.intern()进行一个总结,希望对你有用:

  • JDK6由于保存字符串常量池的是一块固定大小的内存区域(PermGem),所以不建议使用String.intern()。

  • JDk7和8实在堆内存中实现的字符串常量池,这就意味者字符串常量池的限制和整个Java应用的内存一样大。

  • JDK7和8中可以通过参数-XX:StringTableSize设定字符串常量池大小,估算一下你应用中需要池化的不同字符串的数量,建议将常量池的大小设置为这个数量的2倍的一个质数(避免可能的碰撞,假设我们需要池化5000个字符串,那么10000往后最近的质数是10007,即设定-XX:StringTableSize=10007)。这会让String.intern运行在一个常数时间内(性能很高),并且每个缓存字符串所需内存会很小(在同任务量下,显式使用Java WeakHashMap会产生4-5倍多的内存开销)。.

  • JDK6到JDK7u40之间XX:StringTableSize默认值是1009,7u40以后,包括JDK8,这个默认值被提升到60013。

  • 如果你不太确定你应用需要多大的字符串常量池,可以通过JVM参数-XX:+PrintStringTableStatistics,在JVM退出时可以看到字符串常量池的使用情况,从而更合理的调整StringTableSize的值。

近期热文

 
  • 面试官问:MySQL的自增ID用完了,怎么办?

  • ArrayList插入1000w条数据之后,我怀疑了jvm...

  • 蚂蚁二面,面试官问我零拷贝的实现原理,当场懵了…

string比大小_你,确定了解Java的String字符串?_第1张图片

最后,分享一份面试宝典《Java核心知识点整理.pdf》,覆盖了JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上

明天见(。・ω・。)ノ♡

你可能感兴趣的:(string比大小)