杰瑞科技汇

Java字符串大小如何确定?

  1. 对象本身的大小(内存开销)
  2. 的大小(占用的内存)
  3. 如何计算和测量
  4. 影响大小的因素

对象本身的大小(内存开销)

一个空的 String 对象(new String())并不是完全不占空间的,在 Java 中,每个对象都有一些固定的元数据开销。

Java字符串大小如何确定?-图1
(图片来源网络,侵删)

32位 JVM vs. 64位 JVM

JVM 的位数会显著影响对象头的大小。

  • 32位 JVM:

    • 对象头: 8 bytes (Mark Word 4 bytes + 类型指针 4 bytes)
    • 引用类型: 4 bytes
    • 一个空 String 对象大约占用 12 bytes (对象头 8 + char[] 引用 4,需要按 8 字节对齐,所以补 4 bytes,总共 16 bytes?这里需要更精确的解释,见下文)
  • 64位 JVM (默认,且开启 +UseCompressedOops):

    • 对象头: 12 bytes (Mark Word 8 bytes + 类型指针 4 bytes,因为压缩了)
    • 引用类型: 4 bytes
    • 一个空 String 对象的大小计算如下:
      1. 对象头: 12 bytes
      2. value 字段 (char[] 引用): 4 bytes
      3. hash 字段 (int): 4 bytes
      4. coder 字段 (byte): 1 byte
      5. serialVersionUID (long): 8 bytes
      6. serialPersistentFields (ObjectStreamField[]): 4 bytes (引用)
      7. 实例数据总和: 12 + 4 + 4 + 1 + 8 + 4 = 33 bytes
      8. 对齐填充: JVM 要求对象大小必须是 8 字节的倍数,33 bytes 向上取整到 40 bytes。
      9. 一个空的 new String() 对象在 64 位 JVM 上至少占用 40 bytes。

注意: String 类的字段可能因 Java 版本而异。hash 字段在 Java 7u6 之后才被引入以缓存哈希码。coder 字段是在 Java 9 中引入的,用于优化 Latin-1 字符的存储。

Java字符串大小如何确定?-图2
(图片来源网络,侵删)

String 类的字段(以 Java 9+ 为例)

从 Java 9 开始,String 的内部实现发生了重大变化,不再使用 char[],而是使用 byte[] 来节省内存。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    @Stable
    private final byte[] value;
    private final byte coder; // 0 = LATIN1, 1 = UTF16
    private int hash; // 默认为0
    // ... 其他字段
}

一个 String 对象的固定内存开销包括:

  • JVM 对象头
  • byte[] value 引用
  • byte coder
  • int hash
  • 其他可能的字段(如序列化相关字段)

无论字符串内容多长,String 对象本身的固定开销至少是 40 bytes(在典型的 64 位 JVM 上)。


的大小

这部分是动态的,取决于字符串的长度和字符编码。

Java字符串大小如何确定?-图3
(图片来源网络,侵删)

Java 8 及之前版本 (使用 char[])

  • char[] 数组本身有对象头(12 bytes)和长度字段(4 bytes),共 16 bytes 的开销。
  • 每个字符在 char[] 中占用 2 bytes
  • *总大小 = 40 (String对象) + 16 (char[]对象) + 2 字符串长度**

Java 9 及之后版本 (使用 byte[]coder)

这是 Java 对 String 内存优化的一个重要改进。String 会根据内容自动选择编码。

  1. coder == 0 (LATIN1 编码):

    • 适用于所有字符都在 ISO-8859-1 范围内的字符串(如英文字母、数字、常见符号)。
    • 每个字符在 byte[] 中只占用 1 byte
    • byte[] 数组对象的开销是 16 bytes (对象头 12 + 长度 int 4)。
    • *总大小 ≈ 40 (String对象) + 16 (byte[]对象) + 1 字符串长度**
  2. coder == 1 (UTF16 编码):

    • 适用于包含非 Latin1 字符的字符串(如中文字符、表情符号、某些特殊符号)。
    • 这种情况和 Java 8 一样,每个字符在 byte[] 中占用 2 bytes
    • byte[] 数组对象的开销是 16 bytes。
    • *总大小 ≈ 40 (String对象) + 16 (byte[]对象) + 2 字符串长度**

示例对比 (在 64 位 JVM 上):

  • String s1 = "Hello"; (5个 Latin1 字符)

    • String 对象: 40 bytes
    • byte[] 对象: 16 bytes
    • 内容: 5 * 1 = 5 bytes
    • 总计 ≈ 61 bytes
  • String s2 = "你好"; (2个 UTF16 字符)

    • String 对象: 40 bytes
    • byte[] 对象: 16 bytes
    • 内容: 2 * 2 = 4 bytes
    • 总计 ≈ 60 bytes
  • String s3 = new String(); (空字符串)

    • 总计 = 40 bytes

如何计算和测量

手动估算

根据上面的公式,你可以根据你的 Java 版本和字符串内容进行估算。

使用工具测量

最准确的方法是使用专门的工具。

jcmd (JDK 自带)

这是在运行时分析 JVM 的利器。

# 1. 找到你的 Java 进程 ID (PID)
jps
# 2. 使用 jcmd 查看该进程的 GC 根和类的详细信息
#    <PID> GC.class_histogram 会打印所有类的实例数量和总大小
jcmd <PID> GC.class_histogram
# 输出示例 (部分)
...
num     #instances         #bytes  class name
--------------------------------------
   1:         5000         200000  [C  (char[])
   2:         1000          40000  java.lang.String
   ...

这里的 #bytes 就是该类所有实例占用的总堆内存大小。

VisualVM (JDK 自带图形化工具)

  1. bin 目录下运行 visualvm.exe
  2. 连接到你的正在运行的 Java 应用。
  3. 在左侧面板选择你的应用,然后点击 "Sampler" (采样器) 标签页。
  4. 点击 "Heap" 按钮,然后点击 "Start" 开始堆采样。
  5. 让你的应用运行一段时间,执行一些字符串操作。
  6. 点击 "Stop" 停止采样。
  7. 在 "Classes" 视图中,你可以按 "Size" 列排序,查看 java.lang.String 占用了多少内存,以及它的实例数量。

Java Object Layout (JOL) 工具

这是一个非常强大和流行的第三方库,专门用于分析对象内存布局。

添加 Maven 依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

使用示例代码:

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class StringSizeExample {
    public static void main(String[] args) {
        System.out.println("当前JVM信息: " + VM.current().details());
        System.out.println("=====================================");
        // 1. 空字符串
        String emptyString = new String();
        System.out.println("空 String 对象的内存布局:");
        System.out.println(ClassLayout.parseInstance(emptyString).toPrintable());
        System.out.println("空 String 对象总大小: " + VM.current().addressSize() * ClassLayout.parseInstance(emptyString).instanceSize() + " bytes");
        System.out.println("=====================================");
        // 2. Latin1 字符串
        String latin1String = "Hello";
        System.out.println("Latin1 String 对象的内存布局:");
        System.out.println(ClassLayout.parseInstance(latin1String).toPrintable());
        System.out.println("Latin1 String 对象总大小: " + VM.current().addressSize() * ClassLayout.parseInstance(latin1String).instanceSize() + " bytes");
        System.out.println("=====================================");
        // 3. UTF16 字符串
        String utf16String = "你好";
        System.out.println("UTF16 String 对象的内存布局:");
        System.out.println(ClassLayout.parseInstance(utf16String).toPrintable());
        System.out.println("UTF16 String 对象总大小: " + VM.current().addressSize() * ClassLayout.parseInstance(utf16String).instanceSize() + " bytes");
    }
}

JOL 会非常详细地打印出对象的每个字段、偏移量、对齐填充等信息,是学习和理解对象内存布局的最佳工具。


影响大小的因素总结

因素 描述 影响
JVM 架构 (32/64位) 64位 JVM 的指针和对象头通常更大。 64位 JVM 上的对象固定开销更大。
JVM 参数 -XX:+UseCompressedOops (压缩普通对象指针) 会将引用从 8 bytes 压缩到 4 bytes,显著减少内存占用。 开启压缩后,对象引用和对象头会变小,String 对象的固定开销从 56 bytes 降到 40 bytes 左右。
Java 版本 Java 9 引入了 byte[]coder 字段,对 Latin1 字符串进行了内存优化。 Java 9+ 对于纯英文等 Latin1 字符串,内存占用减半。
字符串的长度和字符类型(Latin1 vs. UTF16)直接决定了内部数组的大小。 长度越长,占用越大;包含中文字符等比纯英文字符占用更多。
字符串池 使用字面量创建的字符串(如 "hello")可能会被放入字符串池,同一个字符串在内存中只有一份。 这可以极大地节省内存,但 String 对象本身的固定大小依然存在。

希望这个详细的解释能帮助你全面理解 Java String 的大小!

分享:
扫描分享到社交APP
上一篇
下一篇