写点什么

利用 String.intern 减少相同值的 String 内存占用

作者:lich0079
  • 2022 年 4 月 16 日
  • 本文字数:2071 字

    阅读完需:约 7 分钟

利用String.intern 减少相同值的String内存占用

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


发布于: 刚刚阅读数: 2
用户头像

lich0079

关注

还未添加个人签名 2018.09.17 加入

https://github.com/lich0079

评论

发布
暂无评论
利用String.intern 减少相同值的String内存占用_内存_lich0079_InfoQ写作平台