在运行时 每个 new String()出来的都是独立的对象,即使他们的值都是一样的,如:"BTC", 那么通过 String.intern()可以大量减少此类对象的内存占用
原理
即使值都是 BTC 的 String, 每个反序列化出来的 BTC String 对象在堆上都是不同的。
通过 intern 可以让一些生命周期比较长的对象持有同一份字符串常量池 String,减少内存占用。
利用字符串常量池减少内存占用, 只能用在可变性较少的字符串变量上,如 currency, symbol(这里可引出另一个问题,为什么不用枚举? 枚举对动态扩展性不好)
极客时间 -- 03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据
以下代码测试中,通过使用 String.intern(), 2 千万个的 String 的内存占用从 1000MB 降到了 84MB。
原因在于 String.valueOf()内部是使用了 new String()创建新的对象,而我们提过 intern()方法返回同样的 String 常量池的缓存对象,让外部持有同一个缓存对象,让 new String()创建出来的对象被迅速回收,达到减少内存的目的。
public class StringInternMemoryTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// List l = withOutIntern();
List l = withIntern();
System.gc();
System.out.println((System.currentTimeMillis() - start) + " ms");
printMemory();
}
public static List withOutIntern() {
List l = new ArrayList();
for (int i = 0; i < 20000000; i++) {
l.add(String.valueOf(1));
}
System.out.println("withOutIntern");
return l;
/**
* withOutIntern
* 697 ms
* ##### Heap utilization statistics [MB] #####
* Used Memory:1000
* Free Memory:1899
* Total Memory:2900
* Max Memory:4096
*/
}
public static List withIntern() {
List l = new ArrayList();
for (int i = 0; i < 20000000; i++) {
l.add(String.valueOf(1).intern());
}
System.out.println("withIntern");
return l;
/**
* withIntern
* 1904 ms
* ##### Heap utilization statistics [MB] #####
* Used Memory:84
* Free Memory:203
* Total Memory:288
* Max Memory:4096
*/
}
public static void printMemory() {
int mb = 1024 * 1024;
//Getting the runtime reference from system
Runtime runtime = Runtime.getRuntime();
System.out.println("##### Heap utilization statistics [MB] #####");
//Print used memory
System.out.println("Used Memory:"
+ (runtime.totalMemory() - runtime.freeMemory()) / mb);
//Print free memory
System.out.println("Free Memory:"
+ runtime.freeMemory() / mb);
//Print total available memory
System.out.println("Total Memory:" + runtime.totalMemory() / mb);
//Print Maximum available memory
System.out.println("Max Memory:" + runtime.maxMemory() / mb);
}
}
复制代码
同时也应注意到 耗时从 697ms 到了 1904ms, intern()方法需要去常量池查找,如果有一样的 String 就返回,没有需要 copy 创建一份再返回。
所以 intern()不是没有代价的,要看场景有选择的使用。
大量相同值的 String 对象从哪来的
大部分 String 是在反序列化的过程中产生的, 包括:
kafka 消息从 byte[] 转换成对象
web, grpc 调用
什么时候去做优化
反序列化产生的 String 对象无论怎样都是会产生的,我们能做的优化就是不要长时间持有他们,让他们快速被 GC 掉。需要长期持有的话去持有 String.intern 返回的同一个缓存对象。
如果一个对象只是 request scope 的,这个时候不值得去优化。
但一个 String 如果最后落到了 Order Position 这种长期会存活的对象,这时是可以去优化的。
什么时候绝不能去做优化
一个 String 变量的值是多变,不确定的,这时绝不能去 String.intern()让它复制进入常量池。
如何发现重复的 String 值
对内存做 dump
在 MAT 里,找到 String 对象, group by value
可以看到值为“BTC”的 String 对象有 8000 多个!
接下来可以通过引用查找是哪些对象持有这些 String。
如何优化
/**
* 利用字符串常量池减少内存占用, 只能用在可变性较少的字符串变量上,如 coin, currency
*
* 原理: 即使值都是BTC的String, 每个反序列化出来的BTC String对象在堆上都是不同的
*
* 通过intern 可以让一些生命周期比较长的对象持有同一份字符串常量池String,减少内存占用
*
*/
public class StringInternUtil {
public static String getIntern(String origin) {
return origin == null ? null : origin.intern();
}
}
复制代码
评论