当前位置: 首页 > news >正文

关于java中的String类详解

        好的,我们来深入、详细地探讨一下 Java 中 String 类的各个方面。这是一个非常核心的话题,涉及语言设计、JVM 实现和性能优化。

1. String 类的底层结构

Java 8 及之前:char[]

在 Java 9 之前,String 类的内部是使用一个 char[](字符数组)来存储字符串内容的,同时还包含两个重要的字段:int hash(缓存字符串哈希值)和 int offset(已废弃)。

// Java 8 及之前的简化实现
public final class String {
    private final char value[]; // 用于存储字符
    private int hash; // 缓存 hashCode() 的结果,默认是 0
    // ... 其他字段和方法
}

  • char value[]char 类型在 Java 中占 2 个字节(16位),遵循 UTF-16 编码。这意味着无论字符是常用的拉丁字母(如 ‘a’)还是复杂的辅助平面字符(如一些emoji 😂),每个字符都占用 2 个字节。

  • int hash:由于字符串的不可变性,其哈希值一旦计算就可以被缓存。这极大地提升了像 HashMap 这样依赖 hashCode() 的集合类的性能,因为不需要每次使用都重新计算。

Java 9 及之后:byte[] + 编码标志位

从 Java 9 开始,String 类的内部实现发生了重大变化,改为使用 byte[](字节数组)和一个编码标志字段 coder

// Java 9 及之后的简化实现
public final class String {
    private final byte value[]; // 用于存储字节
    private final byte coder;   // 编码标识 (0 = LATIN1, 1 = UTF16)
    private int hash; // 缓存 hashCode() 的结果,默认是 0
    // ... 其他字段和方法
}

  • byte value[]byte 类型占 1 个字节(8位)。现在字符串的内容以字节形式存储。

  • byte coder:这是一个标志位,它告诉 JVM 如何解读 value[] 数组中的字节。

    • coder = 0:表示字符串内容仅包含 ISO-8859-1/Latin-1 字符集中的字符。这些字符每个都可以用 1 个字节表示。

    • coder = 1:表示字符串中包含至少一个无法用 Latin-1 表示的字符,必须使用 UTF-16 编码。此时,每 2 个字节代表一个 char(即一个代码单元)。

这一改变的核心思想是:根据字符串的实际内容,动态选择更紧凑的存储格式,以节省内存。

2. JVM 中的存储方式

字符串在 JVM 中的存储分为两部分:对象本身 和 字符串常量池

a. 字符串对象(String Object)

无论字符串来自何处,当你在堆(Heap)上创建一个 String 对象时,它和普通对象一样,包含对象头(Mark Word、Klass Pointer)、实例数据(value[]coderhash 等)和对齐填充。

  • 对象头:包含垃圾回收、锁信息等元数据,以及指向其类元数据的指针。

  • 实例数据:就是上面提到的 value[]coderhash 等字段。

  • 对齐填充:为了满足 JVM 对象大小必须是 8 字节的倍数的要求而进行的填充。

这个 String 对象内部的 value[] 数组(在 Java 8 是 char[])也是一个独立的对象,存储在堆上。

b. 字符串常量池(String Table)

字符串常量池是 JVM 为了提升性能和减少内存开销而设计的一种特殊内存区域。

  • 目的:实现字符串的驻留(Interning),避免创建多个内容相同的字符串对象。

  • 位置

    • Java 7 之前:位于方法区(Method Area),永久代(PermGen)中。

    • Java 7 及之后:被移动到了堆(Heap) 中。这样做的好处是垃圾收集器可以管理常量池中的字符串,从而避免永久代大小限制导致的内存溢出问题(OutOfMemoryError)。

  • 工作原理

    1. 当你使用字面量(如 String s = "hello";)创建字符串时,JVM 会首先去字符串常量池中查找是否存在内容相同的字符串。

    2. 如果找到,则直接返回该字符串的引用。

    3. 如果没找到,则在常量池中创建一个新的字符串对象,然后返回其引用。

    4. 使用 new String("hello") 或 intern() 方法也会触发与常量池的交互。

示例:

String s1 = "hello"; // 在常量池中创建或查找
String s2 = "hello"; // 直接找到常量池中的同一个对象
String s3 = new String("hello"); // 在堆上强制创建一个新对象,但内部的 value[] 可能指向常量池中的同一个数组

System.out.println(s1 == s2); // true,引用相同
System.out.println(s1 == s3); // false,引用不同
System.out.println(s1.equals(s3)); // true,内容相同

3. 为什么使用 final 修饰 String 类?

将 String 类声明为 final 主要有三个核心原因:

  1. 安全性(Security)

    • Java 的许多关键类(如 ClassLoader)、参数(如文件名、URL)都使用字符串。如果字符串是可变的,就可以通过修改字符串内容来绕过安全检查和改变程序行为,造成严重的安全漏洞。final 防止了通过创建恶意子类来破坏这些假设。

  2. 不可变性(Immutability)的基石

    • final 阻止了继承,从而防止子类覆盖方法(如 length()charAt())来破坏其不可变性的承诺。不可变性带来了巨大的好处:

      • 线程安全:不可变对象可以在多线程间自由共享,无需同步,没有竞态条件问题。

      • 哈希缓存:可以安全地缓存哈希码,这对于 HashMapHashSet 等集合的效率至关重要。

      • 字符串常量池:只有不可变的对象才能被安全地缓存和重用。如果可以修改,那么一个地方的修改会影响到所有“共享”该字符串的地方,这是灾难性的。

  3. 性能优化

    • 除了哈希缓存,编译器和其他运行时优化可以利用不可变性进行各种优化,例如在字符串连接时进行优化。

4. 为什么从 char[] 改为 byte[]?

这是 Java 9 中一个非常重要的性能优化,主要目的是大幅减少字符串的内存占用

  1. 节省内存(Memory Footprint Reduction)

    • 统计表明,在大多数实际应用中,字符串可以占到整个应用内存的 20% 到 40%

    • 同样统计表明,大量的字符串内容只包含 Latin-1 字符(即所有英文字母、数字和西欧语言符号)。这些字符用 1 个字节足以表示,但之前却一直占用 2 个字节。

    • 改变带来的收益:对于纯 Latin-1 内容的字符串,内存占用直接减半(char[].length * 2 bytes -> byte[].length * 1 bytes)。即使是包含非 Latin-1 字符的字符串,其开销也与之前持平(byte[].length * 2 bytes,因为 coder 标志为 1)。

  2. 适应硬件发展(Hardware Efficiency)

    • 更紧凑的内存布局意味着:

      • 更少的堆内存压力:可以创建更多字符串或减少 GC 频率。

      • 更好的缓存局部性(Cache Locality):CPU 的 L1/L2/L3 缓存可以加载更多的字符串数据,从而减少缓存未命中(cache misses),提升程序运行速度。

  3. 平滑的权衡(A Smooth Trade-off)

    • 这个改动并非完美。对于包含非 Latin-1 字符的字符串,String 类的一些方法(如 charAt(int index))需要额外的逻辑来判断编码并提取正确的 char,这会引入一点点性能开销。

    • 然而,省下的内存远大于这点微乎其微的性能开销。并且,由于缓存局部性更好,整体性能往往是提升的。

总结

特性Java 8 及以前Java 9 及以后原因与好处
底层结构char[]byte[] + coder节省内存:针对 Latin-1 字符串内存减半。
不可变性final 类,不可变final 类,不可变安全线程安全支持常量池哈希缓存
JVM 存储对象在堆,常量池在堆对象在堆,常量池在堆便于GC管理,避免永久代内存溢出。

Java 团队通过将 String 底层从 char[] 改为 byte[],在保持所有原有 API 和行为完全不变的前提下,巧妙地利用统计规律,为绝大多数应用带来了立竿见影的内存节省和性能提升,这是一个非常出色的工程优化典范。

推荐一个非常好用的工具集合:在线工具集合 - 您的开发助手


文章转载自:

http://6ChNju6b.stpkz.cn
http://N1pxrshf.stpkz.cn
http://udeyKRec.stpkz.cn
http://jwjIdEfO.stpkz.cn
http://APbUxVV6.stpkz.cn
http://idTMbsLp.stpkz.cn
http://BZEyHCfO.stpkz.cn
http://lFFdtPuS.stpkz.cn
http://rDMrYlJg.stpkz.cn
http://IQhzvCHv.stpkz.cn
http://mPjHFan9.stpkz.cn
http://QrYvpMup.stpkz.cn
http://3iLqUXks.stpkz.cn
http://ZjG8kahZ.stpkz.cn
http://sgu5vq02.stpkz.cn
http://JUNRq1Yc.stpkz.cn
http://ZteArV6t.stpkz.cn
http://vseDw0c0.stpkz.cn
http://VftjDHvx.stpkz.cn
http://pNodbQ0i.stpkz.cn
http://gqjxqg4n.stpkz.cn
http://HTUSvMsE.stpkz.cn
http://RP7T2X5J.stpkz.cn
http://xMRHm5gT.stpkz.cn
http://TNABAgwu.stpkz.cn
http://38lCDPzV.stpkz.cn
http://m6bXhPSR.stpkz.cn
http://BBmP4VhE.stpkz.cn
http://PNItfGin.stpkz.cn
http://AVWOwp2R.stpkz.cn
http://www.dtcms.com/a/381531.html

相关文章:

  • S3C2440 ——UART和I2C对比
  • TDengine 数据写入详细用户手册
  • 校园电动自行车管理系统的设计与实现(文末附源码)
  • HarmonyOS 应用开发深度解析:基于 ArkTS 的现代化状态管理实践
  • 【大语言模型 58】分布式文件系统:训练数据高效存储
  • [code-review] AI聊天接口 | 语言模型通信器
  • 力扣刷题笔记-删除链表的倒数第N个结点
  • 代码审计-PHP专题原生开发SQL注入1day分析构造正则搜索语句执行监控功能定位
  • dots.llm1:小红书开源的 MoE 架构大语言模型
  • --gpu-architecture <arch> (-arch)
  • uniapp动态修改tabbar
  • Spring Boot 集成 Flowable 7.1.0 完整教程
  • 教你使用服务器如何搭建数据库
  • Kafka如何配置生产者拦截器和消费者拦截器
  • uniapp:根据目的地经纬度,名称,唤起高德/百度地图来导航,兼容App,H5,小程序
  • 欧拉函数 | 定义 / 性质 / 应用
  • 【更新至2024年】1996-2024年各省农业总产值数据(无缺失)
  • 财报季观察|消费“分野”,燕之屋(1497.HK)们向上生长
  • 机械制造专属ERP:降本增效与数字转型的关键
  • 基于node.js+vue的医院陪诊系统的设计与实现(源码+论文+部署+安装)
  • 【大语言模型 59】监控与日志系统:训练过程全面监控
  • HIS架构智能化升级编程路径:从底层原理到临床实践的深度解析(下)
  • Node.js中package.json详解
  • 当AI遇上数据库:Text2Sql.Net如何让“说人话查数据“成为现实
  • 数据结构8——双向链表
  • 问卷系统自动化测试报告
  • Python 的函数柯里化(Currying)
  • 渗透测试信息收集详解
  • 【连载3】C# MVC 异常日志进阶:结构化日志与性能优化技巧
  • 冯诺依曼体系:现代计算机的基石与未来展望