StringTable位置
1.6 及其之前,StringTable 位于 堆内部的永久代中,只有在 full GC 的情况下才会进行垃圾回收,这就造成很多已经无用的字符串对象仍存在于堆中,占用内存资源。
1.8 及其之后,StringTable 被移动到JVM 外部、内存中的元空间中,在这里GC会正常进行。
示例:
1.6
1.8
未直接显示元空间GC,是因为
当采用了98%的时间来垃圾回收,还不能回收2%的堆内存时,会报OutOfMemoryError
。
我们可以通过 -XX +UseGCOverheadLimit 参数来确定是否需要这个功能,使用减号就可以删除这个功能。
这下可以看到这下报了堆空间不足的错误。
StringTable垃圾回收
StringTable会进行垃圾回收的,这可能与大家的印象不符合。现在使用示例来证明。
首先执行一个什么也不做的程序,来打印一下StringTable初始状态。
- 启动时增加参数,以便打印StringTable相关数据
-Xmx10m
设置堆大小为10M,方便触发垃圾回收-XX:+PrintStringTableStatistics
设置打印StringTable数据-XX:+PrintGCDetails -verbose:gc
设置打印GC数据
然后启动程序
- 查看打印出来的参数
- 打印GC信息:Heap 开头的是因为要打印GC数据,所以打印堆占用情况
- SymbolTable 符号表,存放类名、方法名等
- StringTable 数据,保存字符串常量对象
StringTable 的实现是哈希表,实现方式是数组+链表,而数组的每个元素就叫作桶bucket。可以看到图中,桶的个数是60013个,条目的个数是1754个,字符串对象的个数也就是1754个
现在执行一段代码,看看填入字符串对象后,StringTable未GC时的样子。
现在执行这样一段代码,循环100次,创建100个字符串对象,并加入到StringTable 中。由于桶数量远大于放入的字符串的数量,所以不会GC,只是StringTable中的对象数量增加了。
可以看到StringTable 中多了100 个entry,变成1854个条目了。
现在执行一段代码,使StringTable内的字符串对象超出StringTable容量。现在加大生成的字符串,生成10000个,并且生成后也不保存到list中,使其成为没有引用的垃圾。
可以看到StringTable 中只存了7000多个字符串。而拉到最上面可以看到,系统经过了垃圾回收GC。
到这里就可以说明,StringTable也会执行垃圾回收。
StringTable性能调优
-
调整-XX:StringTableSize=桶个数
StringTable 是由哈希表实现的,字符串对象按照哈希值进入对应的桶中。当桶的数量增大,哈希碰撞次数就会减少,链表也会缩短,插入和取出的时间也会缩短,就能提升性能了。
接下来看一个示例
将文件中的48万个单词都读入内存,并使用 instern() 函数,将数据放入StringTable。并给这个过程进行计时。
可以看到,设置桶的数量为200000个时,字符串导入的时间是0.4s。这是因为平均一个桶的链表长度为480000/200000=2.4个,所以时间很快。
现在设置桶数量=1009,可以看到时间一下增长了,变成了12s。
总的来说,将StringTable 的桶数量设置越大,StringTable 的性能越好。 -
考虑字符串对象是否入池
未入池的情况,程序中读取文件十次,创建了对象,但是并不放到StringTable中,只是放在堆中。
使用Java VisualVM查看初始状态
内存中 char[] 和 String 对象占用大概1M。
执行后,字符串达到300M占用。
可以看到只新建字符串对象、而不放入StringTable,则所有对象占用堆极大空间。
字符串入池的情况:将读取到的字符串全都使用 instern() 放到StringTable中,实现复用StringTable中的字符串。
可以看到String 和 char[] 合起来总共30多M。
总结一下:字符串入池,使用instern()函数,获取StringTable 中的已有的字符串对象,就可以复用字符串,减少在堆中的内存占用。