在运行时 每个 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(); }}
复制代码
评论