字节二面:字符串常量池如何优化?

54 阅读4分钟

字节二面:字符串常量池如何优化?

本文禁止转载!

1. intern 的问题

对于 JVM 有一定程度了解的开发者都知道字符串常量池,Java 编译器在编译期间可以直接获得或者简单计算得到的字符串会存在这个池子中。

简单来说,intern 方法实现了 ConcurrentMap#putIfAbsent,返回值为第一次写入池中的对象(引用)。intern 方法有很多问题:

  • 性能一般:Intern 方法需要支持多线程操作,会带来多线程下同步或者锁的开销。其底层实现哈希表桶位数为固定值,更改桶位数需要改 JVM 参数,一个简单的功能需要用户了解很多复杂的知识。
  • 开发人员通常不知道应该在代码的哪些地方使用 String.intern(),或者难以找到负责代码的人员并对代码进行更新。
  • 当前 intern的实现不具备良好的扩展性,只支持 String 类型
  • 字符串常量池的字符串难以回收

JEP-192 提到了有些开发者会搞一些花活:

可以分析现有应用程序,找出通常存储重复字符串对象的位置。使用诸如 java.lang.instrument 之类的框架在合适的位置注入 String.intern() 调用。这种方法的优点是不需要更新源代码,而是动态地更改字节码。

实际上,这些小聪明看似有效,实则问题重重:

  1. 如果在热点路径中注入 intern() 调用,可能会显著影响性能。
  2. 源代码变化后可能需要重新进行分析,这可能是昂贵且需要手动的工作

然而,很多面试官或者孔乙己揪着 intern 不放,要求面试者详细说明 intern 在不同版本的底层实现。实际上,对 字符串进行 intern 又是 JVM ( Java 语言) 特殊的处理方式,其目的是为了减少重复字符串的内存占用,然而,就和 Object 类中的许多方法一样,不止增加了学习成本,而且易错,甚至可能会酿成大错。

2. Guava Interner

Guava 的 Interner 是一个用于管理和重用对象实例的实用工具,类似于 String.intern() 的功能,但更加灵活和高效。Interner 提供了一种机制来确保在内存中对于相同的对象只保留一个实例,从而减少内存使用和提高性能,特别是在需要频繁创建相同对象的场景中。

以下是 Guava Interner 的一些关键特性和使用方法:

  1. 类型安全

    Interner 是泛型的,可以用于任何类型的对象,而不仅仅是字符串。

  2. 实现方式

    Guava 提供了 Interner 接口和 Interners 工具类来创建 Interner 实例。常用的实现包括 WeakInternerStrongInterner,分别使用弱引用和强引用来管理对象。

  3. 使用场景

    当需要频繁创建相同的对象实例时,使用 Interner 可以显著减少内存消耗。在需要确保对象唯一性时,Interner 可以帮助避免重复实例化相同的对象。

  4. 基本用法

    Interner<String> interner = Interners.newWeakInterner();
    String internedString = interner.intern("example");
    

    上述代码创建了一个 WeakInterner,并将字符串 "example" 放入池中。如果池中已经存在相同的字符串实例,则返回现有实例。

  5. 弱引用 vs 强引用

    WeakInterner 使用弱引用,允许垃圾回收器回收不再被使用的对象,适合内存敏感的应用。

    StrongInterner 使用强引用,确保对象一直存在于内存中,适合需要长期缓存的对象。

通过使用 Guava 的 Interner,开发者可以更有效地管理对象实例,优化内存使用,并提升应用程序的性能。

2. 1代码示例

public class InternerDemo {
    public static void main(String[] args) {
        // 配置 Interner 为弱引用,并设置并发级别
        Interner<String> interner = Interners.newBuilder()
            .weak()                // 配置为弱引用
            .concurrencyLevel(4)   // 设置并发度
            .build();              // 构建 Interner 实例
​
        String internedString1 = interner.intern(new String("example"));
        String internedString2 = interner.intern(new String("example"));
​
        // 验证两个字符串是否是同一个实例
        System.out.println(internedString1 == internedString2); // 输出 true
    }
}

2.2 底层实现分析

Interners 提供的工厂方法实现使用了 MapMakerMapMaker 是 Guava 提供的一个强大的工具,用于创建具有特殊特性的并发映射(ConcurrentMap)。它允许开发者创建带有弱键或弱值引用的映射,这对需要自动管理内存的应用程序特别有用。底层实现使用了和 JDK7 中 ConcurrentHashMap 一样的分段锁思想(并发度思想)。关于非强引用相关知识,建议看我之前写的《深入理解 Java 引用类》

3. 最佳实践

  1. 不推荐使用 intern 方法,除非你是JVM领域的专家,专家也不需要看这篇文章。不要试图在 intern 相关问题上做文章或者搞优化。
  2. 使用 Guava 中 Interner 接口,注意强引用和弱引用的选择,注意并发度的配置。
  3. 针对G1垃圾回收器,根据实际情况选择开启 String Deduplication。