使用 UseStringDeduplication 来减少 Java String 的内存占用量
我们分析 Java heap dump 的时候, 经常发现里面包含很多 java.lang.String, 可是让我们回想到底哪里用了这么多 String 的时候, 确实很难列举. 如此多的 String 肯定占用了很多宝贵的内存空间, 那么有什么办法能减少 String 的空间占用呢?
下面一个 heap dump 的 object histogram 的截图, 可以看到 String 的数量仅次于 byte[], 位居第二.
如果你去研究为啥这么多 byte[], 最终你会发现, 其实是 String 太多, 每个 String 对象的背后也是一个byte[].
String 也是 immutable 的对象, 也就是说你对 String 的任何修改会直接创建另外一个新的对象.
另外, 我们会发现, 其实我们的内存里面有很多重复的 String. 通过 MAT 的 “Java Basics” -> "Group by value", 我们对 java.lang.String
进行分组, 可以看到很多重复的字符串.
如下图, 在一个只有 158 MB的 heap 里面, https
这个 String 竟然有 70397 个实例对象.
如果我们添加 -XX:+UseStringDeduplication
, 经过一段时间的稳定运行后, 我们可以看到, 虽然 String 还是那么多, 但是 byte[] 已经大幅减少:
对比上图我们发现:
- String 数量还是差不多, 但是 byte[] 明显减少.
- String 对象 retained size 明显减少, 就是因为它们引用的 bytes[] 很多都合并了.
我们以 https 这个字符串为例, 可以看到他们引用的 byte[] 都是一个:
对比 intern()
使用 intern()方法, 返回的字符串常量都是字符串常量池里面的同一个.
使用 UseStringDeduplication, 一开始是在EDEN 区域分配的时候, 每个String 都是新的, byte[] 也是不一样的, 当被GC 回收次数达到 StringDeduplicationAgeThreshold
的时候, 会有后台线程处理, 把 bytes[] 指向常量池里的那个字符串. 但是 String 对象本身还是之前的.
uint StringDeduplicationAgeThreshold = 3 {product} {default}
bool UseStringDeduplication = false {product} {default}
bool PrintStringTableStatistics = false {product} {default}
为什么 UseStringDeduplication 默认是关闭的
- String 的 byte[] 一开始还少正常每个都分配空间的, 等被回收次数到达
StringDeduplicationAgeThreshold
后, 才会有后台线程更改 byte[], 所以需要在 GC 后额外占用时间. - 更改这些指针还需要额外CPU.
- 需要额外内存记录被回收次数.