JAVA类加载机制

作用: 将class文件加载到JVM内存中;

DK8类加载体系

1、三大核心类加载器:

启动类加载器(Bootstrap ClassLoader): 由C++开发,是JVM虚拟机的⼀部分,本身不是JAVA类。加载String,Int等基础类。
扩展类加载器(ExtClassLoader): 加载⼀些扩展类。 可通过-D java.ext.dirs另⾏指定⽬录。
应用程序加载器(ApplicationClassLoader): 加载用户类路径(ClassPath)下的jar包,即我们自己编写的Java代码。

2、类加载器实现类:

                Object
                  ↑
             ClassLoader (抽象类)
                  ↑
        SecureClassLoader (抽象类)
                  ↑
             URLClassLoader (具体类)
                  ↑
      ┌───────────┴───────────┐
      ↓                       ↓	
ExtClassLoader             AppClassLoader
(扩展类加载器)              (应用类加载器)
(sun.misc.Launcher$)       (sun.misc.Launcher$)

3、双亲委派模型(Parents Delegation Model)

过程1: 当一个类加载器收到类加载请求时,首先查看自己的缓存中是否已经加载过该类。
过程2: 如果缓存中存在,则直接返回该类的实例。如果缓存中不存在,则将请求委派给父类加载器去处理。
过程3: 父类加载器执行相同的流程。如果父类加载器无法加载这个类(即在其搜索路径下找不到),则子类加载器才会尝试自己加载。
过程4: 子类成功加载后,将加载的类放入自己的缓存中,下次再请求时可直接返回。

作用: 保证java核心类库不被用户定义的类覆盖且父类加载过的类子类不在重复加载;

4、类加载机制总结:
1:缓存: 每个类加载器对自己加载过的类都有一个缓存
2:双亲委派机制: 向上委托查找,向下委托加载。
3:沙箱保护机制: 不允许应用程序加载系统内部类,保护jdk核心类不被不会被应用覆盖。

类加载的生命周期

JVM 读取 .class 字节码,并生成 java.lang.Class 对象的过程,共 7 个阶段:

1、加载: 通过类加载器将class文件加载至JVM中
2、验证: 验证class文件是否符合JDK规范
3、准备:静态变量分配内存,并赋初始值(零值),此时为半初始化状态。
4、解析: 将符号引用(类名、方法名)替换为直接引用(内存地址)
5、初始化: 执行 ():静态变量赋值、执行静态代码块,初始化当前类父类
6、使用: 实例化对象、调用方法、访问属性
7、卸载: 该类不再被使用 → 从方法区卸载,由 GC 执行(通常和自定义类加载器相关)

半初始化状态 :jdk在处理静态变量时会给这个属性赋初始值,目的是占住内存。如果静态变量使用final关键字修饰表示值永远不可变,会直接赋真值。

java.lang.Class对象: Class 对象是 JVM 为每个已加载的类创建的元数据对象,它描述了该类的结构信息(类名、方法、字段、构造器等),是反射的入口。每个类在 JVM 中有且只有一个 Class 对象,存储在堆中。用于反射、类加载、动态代理等。这个 Class 对象是加载阶段的产物,后续可以通过 对象.getClass() 或 类名.class 访问。

注意:
类本身(静态结构)加载到方法区的过程,只执行一次。
验证阶段、准备阶段、解析阶段一起称为 Linking链接过程
准备阶段静态变量赋零值,初始化阶段静态变量赋真值;
java.lang.Class 对象存储在堆中,静态变量随class对象一起存储。

对象创建的主要流程

执行new MyObject() 时,JVM 在做什么

1、类加载检查 :检查类是否已被加载、连接、初始化;如果没有,先执行类的生命周期(前 5 阶段)
2、分配内存 :为新生对象分配内存。类加载后就可以确定所需内存大小,在堆空间划分已确定大小的内存。
3、初始化 : 实例变量赋零值,如 int a = 0、boolean b = false、引用 = null
4、设置对象头 :设置对象的Mark Word(哈希码、GC分代年龄、锁信息)、类元数据指针信息。
5、执行<init>方法 :为实例变量赋值,并执行构造方法中的代码

注意:
对象创建根据类元信息在堆中生成具体实例的过程,可重复执行。

对象创建与类的生命周期

类的生命周期 是 对象创建 的“前置流程”

执行new MyObject() 时:
首先进行类的生命周期检查 类是否已经加载,
如果未加载进行 **类生命周期(仅首次执行)加载 → 验证 → 准备 → 解析 → 初始化
然后进行
对象创建(每次 new 都执行)**分配内存 → 零值 → 对象头 →

核心联系: 对象的创建时 如果该类未加载触发类的生命周期。

划分内存的方法

1:指针碰撞(Bump the Pointer)(默认用指针碰撞)
堆内存是绝对规整的,使用指针将内存区域分为已使用区域空闲区域,划分内存时只需要将指针向空闲区域移动与对象大小相等距离
如:复制算法,标记-整理算法

2:空闲列表(Free List)
JVM维护一张表,记录内存中所有空闲区域的起始位置与大小,划分内存时查找出足够大小的内存分配给对象实例,并更新表记录。
如: 标记-清除算法(CMS 老年代)

划分内存时线程并发问题

当多个线程同时使用指针碰撞分配对象时会产生并发安全问题

1、CAS(compare and swap)

使用CAS+失败重试的方式更新分界指针不成功就重试(自旋)。保证操作的原子性。

2、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

每个线程在Java堆中预先分配一小块内存,线程在自己的 TLAB 内使用指针碰撞分配对象,完全无竞争。
­
参数:
XX:+/­UseTLAB :是否使用TLAB
­XX:TLABSize :指定TLAB大小

对象头

HotSpot虚拟机中,对象在内存中可分为三块区域:对象头(固定长度)、实例数据(成员变量)、对其填充(补齐为 8 的倍数)。
对象头包括两部分信息:
1:Mark Word 存储对象本身运行时数据,如hash码、gc年龄、锁状态标志、偏向线程ID等
2:Klass pointer类型指针:对象指向类元数据的指针,这个指针可以确定这个对象是哪个类的实例。
3:数组长度:只有数组对象才有。

指针压缩
启用指针压缩:­XX:+UseCompressedOops(默认开启),
禁止指针压缩::XX:­UseCompressedOops

什么是指针压缩:
指针压缩是 HotSpot 在 64 位 JVM 中,将堆内存中的引用从 8 字节压缩到 4 字节的优化技术。

对象内存分配

对象栈上分配

栈上分配是 JVM 基于逃逸分析的优化技术,将不逃逸的对象拆解为基本类型变量(标量替换),分配到栈帧中。对象不再在堆上分配,也不再经过 GC,方法结束时自动销毁,从而显著降低 GC 压力。JDK 8 中默认开启

栈上分配依赖于逃逸分析和标量替换

参数:
开启逃逸分析(-XX:+DoEscapeAnalysis)JDK7之后默认开启
开启标量替换(-XX:+EliminateAllocations)JDK7之后默认开启

**栈上分配:**对象分配到栈上,方法结束后直接销毁,不经过GC
**逃逸分析:**当一个对象在方法中被定义后,如果没有被外部方法所引用此时未发生逃逸,如果被外部方法引用此时发生逃逸
标量替换: 不创建对象,直接将其成员变量拆解为多个局部变量
标量: 不可再分的基本类型(int、long、reference 等)
聚合量: 可以拆解的对象(如 User 对象)

对象在Eden区分配

大多数情况下,对象直接分配在新生代中Eden区域,当Eden区域剩余空间不足以满足分配时,会触发一次minor Gc,此时可能会有99%的对象成为垃圾对象被回收,剩余存活对象移动到survivor区域,等下一次Eden区域满了以后又会触发一次minorGc,把Eden与survivor区域垃圾对象回收,将剩余存活对象全部移动到另一块为空survivor区域。

因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可。

参数
Eden与survivor区内存大小比例自动变化(默认开启):-XX:+UseAdaptiveSizePolicy

Minor GC/Young GC: 指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC: 一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。

对象进入老年代

1、大对象直接进入老年代

大对象就是需要大量连续内存空间的对象

原因:避免为大对象分配内存时的复制操作而降低效率。如果在新生代长期存活会在eden与survivor区域来回移动且大对象在 Young GC 时复制成本极高。

参数
设置大对象的大小:-XX:PretenureSizeThreshold (只在 Serial 和ParNew两个收集器下有效)

2、长期存活的对象将进入老年代

JVM中每个对象有一个对象年龄计数器,当对象在Eden区经过一次Minor Gc存活,移动到survivor区后年龄=1,之后对象在 Survivor 中每经过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁),就会被晋升到老年代中。

参数
对象晋升到老年代的年龄阈值-:XX:MaxTenuringThreshold

3、对象动态年龄判断

Minor GC 之后,有对象的Survivor区域,计算每个年龄的对象大小,从小到大累加,当加到某个年龄的对象总大小超过Survivor区域内存大小的50%,该年龄及以上年龄的所有对象直接晋升老年代。

避免 Survivor 区积压过多对象,防止晋升不及时导致的 GC 效率下降。

参数
Survivor 区填充阈值:-XX:TargetSurvivorRatio

4、老年代空间分配担保机制
Minor GC 之前,JVM 检查老年代剩余空间是否 ≥ 年轻代所有对象的总大小。如果不满足,则根据历史每一次Minor Gc后进入老年代对象的平均大小决定是否允许本次 Minor GC 执行。

Minor GC 前
    ↓
检查老年代剩余空间 ≥ 年轻代所有对象总大小?
    ├─ 是 → 安全 → 执行 Minor GC
    └─ 否 → 存在风险
            ↓
        检查是否允许冒险(-XX:-HandlePromotionFailure 是否开启)
            ├─ 否 → 直接 Full GC
            └─ 是
                ↓
            检查老年代剩余空间 ≥ 历次晋升到老年代的平均大小?
                ├─ 是 → 冒险执行 Minor GC
                └─ 否 → 直接 Full GC

如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc

参数
-XX:-HandlePromotionFailure :是否查看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。

对象内存回收

引用计数法:

每个对象维护一个引用计数器,有引用指向时 +1,引用失效时 -1,计数器为 0 时回收。

优点:
实现简单,判断高效
实时回收(引用为 0 立即回收)

缺点:
存在对象之间相互循环引用问题
每次赋值都要维护计数器(开销大)
需要额外空间存储计数器

可达性分析算法:

从 GC Roots 出发,遍历所有引用链,可达的对象存活,不可达的对象可回收。

**GC Roots根节点:**线程栈的本地变量、静态变量、本地方法栈的变量等等

优点:
解决循环引用
GC Roots 以外的对象都可回收
更精确

缺点:
需要遍历整个引用链(耗时)
需要 STW(枚举根节点时)

常见引用类型

强引用: 最常见的引用:Object obj = new Object(),只要强引用存在,GC 永不回收。

 public static User user = new User();

软引用: 内存不足时回收,适合做缓存。

将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放
新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。

public static SoftReference<User> user = new SoftReference<User>(new User());

弱引用: 下一次 GC 立即回收,不管内存是否充足。

将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用

使用场景 WeakHashMap、ThreadLocal

public static WeakReference<User> user = new WeakReference<User>(new User());

虚引用: 无法通过虚引用获取对象,仅用于跟踪对象被回收的状态。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

finalize()方法

finalize() 是 Object 类的一个 protected 方法,在对象被 GC 回收之前调用,用于执行最后的资源清理。

**作用:**最终判定对象是否存活

当可达性分析算法中的不可达的对象,对象死亡时,至少要经历再次标记过程。
1. 第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,对象将直接被回收。
2. 第二次标记
如果这个对象覆盖了finalize方法,并重新与引用链上的任何的一个对象建立关联,在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

注意:
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链
一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。

如何判断一个类是无用的类
类需要同时满足下面3个条件才能算是 “无用的类” :
1、该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  • Java 堆中不存在该类的任何实例,没有 new 出来的对象存活,包括该类本身的实例 + 其子类的实例

2、加载该类的 ClassLoader 已经被回收。

  • 通常只在 自定义 ClassLoader 场景下出现
  • ClassLoader 对象本身在堆中不可达,一般发生在 ClassLoader 不再被引用后,GC 回收它。
  • 系统类加载器(App / Ext / Bootstrap)永远不会被回收

3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • java.lang.Class 对象没有在任何地方被引用
  • 没有通过反射持有该 Class 对象,无法通过任何方式访问该类的方法、字段

类卸载只在 Full GC 时发生,典型场景是热部署、OSGi、自定义 ClassLoader。系统类(如 String)永远不会被卸载。

JVM整体结构及内存模型

JVM虚拟机主要由类装载子系统、运行时数据区、字节码执行引擎组成,运行时数据区(jdk8)由本地方法栈、虚拟机栈、堆、方法区、程序计数器组成。

1:类装载子系统(Class Loader Subsystem)

功能作用:

  • 负责查找并加载 .class 文件,将其转换为 java.lang.Class 对象放入元空间。
  • 三个阶段:加载(Loading)→ 连接(Linking)→ 初始化(Initialization)。

2:字节码执行引擎(Execution Engine)
功能作用:

  • 将字节码指令解释为机器码执行,或通过 JIT 编译器将热点代码编译为本地代码缓存。
  • 管理栈帧的创建、操作数栈的运算、程序计数器的更新。

3:运行时数据区

1、程序计数器(Program Counter Register)

功能作用:

  • 线程私有,每个线程都有自己的程序计数器。
  • 记录当前线程执行的字节码指令地址(行号)。
  • 字节码解释器通过改变计数器的值来选取下一条要执行的指令。

备注:唯一不会 OOM 的内存区域

2、虚拟机栈(Java Virtual Machine Stack)

功能作用:

  • 线程私有,生命周期与线程相同。
  • 每个方法调用对应一个栈帧(Stack Frame),方法执行完则栈帧出栈(FILO先进后出)。
  • 存储:局部变量表、操作数栈、动态链接、方法出口。

栈帧内部结构

  • 局部变量表:存放方法参数和方法内定义的局部变量(包括对象引用、基本类型)。编译时确定大小。
  • 操作数栈:用于存放计算过程中的中间结果,执行字节码指令时作为操作数入栈出栈
  • 动态链接:指向运行时常量池中该方法的符号引用。支持多态(晚期绑定),将符号引用转换为直接引用。
  • 方法出口:方法正常或异常返回后,返回到上层方法的执行状态(程序计数器、栈帧)

备注:

  • 递归过深或栈帧过多会导致 StackOverflowError
  • 动态扩展失败(如大量线程)会导致 OutOfMemoryError
  • 局部变量表中的对象引用指向堆中的实例基本类型直接存储值

3、本地方法栈(Native Method Stack)

功能作用

  • 线程私有,与虚拟机栈类似,但为 Native 方法服务(如 Object.hashCode() 底层 C++ 实现)。
  • 在 HotSpot 中,本地方法栈与虚拟机栈合并,使用同一块内存。

备注:

  • 同样可能抛出 StackOverflowError 和 OutOfMemoryError。
  • NI(Java Native Interface)调用时使用。

4、堆(Heap)

功能作用:

  • 线程共享,JVM 中最大的一块内存。
  • 存储:所有类的实例对象、数组、静态变量(JDK 8+)、java.lang.Class 对象。

备注:

  • GC 的主要管理区域(Minor GC → Young,Mixed GC → Young + 部分 Old,Full GC → 全堆)。
  • 静态变量在堆中(JDK 8),不在元空间。
  • 堆内存不足 → OutOfMemoryError: Java heap space。
  • TLAB(线程本地分配缓冲区) 位于 Eden 区,用于无锁分配对象。

5、方法区 / 元空间(Metaspace)

功能作用:

  • 线程共享,存储类结构信息。
  • JDK 8 之前叫“永久代(PermGen)”,JDK 8 改为元空间(Metaspace),使用本地内存(不在堆中,大小受物理内存限制)

存储内容
1:类元数据(Klass)如类的基本信息:类名、父类、接口、字段、方法等,但都是C++ 对象
2:运行时常量池,编译期生成的符号引用(类、方法、字段名)、字面量(字符串、final 常量)

备注:

  • JDK 7 将字符串常量池和静态变量移到了堆,JDK 8一样。
  • 元空间大小默认无上限但受物理内存限制,元空间满会报OutOfMemoryError: Metaspace,可通过 -XX:MaxMetaspaceSize 限制。

JVM内存参数

-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 元空间允许使用的最大容量(硬上限)。达到后若无法通过Fullgc释放空间,则 OOM。

调整元空间大小需要 Full GC
元空间中的类元数据(Klass)在内存中是连续分配的,扩容或缩小时需要移动类元数据、更新指针、清理碎片。这个过程类似于老年代的“标记-整理”,必须暂停所有用户线程(STW),因此是非常昂贵的操作。

当类加载导致元空间使用量超过 MetaspaceSize(默认 21M),JVM 会立即触发 Full GC 来尝试回收未使用的类加载器和类元数据,并判断是否需要扩容或缩容。

如果在 GC 后释放了大量空间,JVM 会降低 MetaspaceSize 阈值(延迟下次 GC);如果释放很少,JVM 会提高阈值(但不超过 MaxMetaspaceSize),以减少未来 GC 频率

MetaspaceSize 和 MaxMetaspaceSize 设为相同的大值
启动时类加载较多,如果 MetaspaceSize 默认 21M 太小,很快会触发 Full GC 扩容。每次扩容都会引发 Full GC,严重影响启动速度
将两个值设为相同(比如 256M),JVM 就不会因为达到阈值而反复触发 Full GC 去调整元空间大小。元空间一开始就申请到最大值,之后即使类加载再多,也不会触发因元空间扩容导致的 Full GC(除非真的超出 256M)

‐Xms2048M :堆内存的初始分配大小
‐Xmx2048M :堆内存的最大分配大小
‐Xmn1024M :年轻代(Young Generation)的初始及最大大小
‐Xss512K:每个线程的栈空间大小

常量池

Class常量池

存储在 .class 字节码文件中,编译时生成,文件中包含文本字符串、final 常量值、基本类型字面量、类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等信息

**作用:**为 JVM 在类加载时提供原始数据,后续被解析为直接引用。

常量池中主要存放两大类常量:字面量和符号引用
字面量:指由字母、数字等构成的字符串或者数值常量(如:int a=1)
符号引用:如类和接口的全限定名 、字段的名称和描述符 、方法的名称和描述符等

运行时常量池

类加载后,Class 文件常量池进入方法区(JDK 7 永久代 / JDK 8 元空间),成为运行时常量池。包含从 Class 常量池转换而来的符号引用和字面量,运行时动态生成的常量(如 String.intern() 添加的字符串实例引用,注意:实际字符串对象在堆,池中存引用)。

作用: JVM 执行字节码时,从运行时常量池查找符号引用并解析为直接引用。

字符串常量池

字符串操作
1、直接赋值字符串

String s = "aaa";  // s指向常量池中的引用

创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象;
如果有,则直接返回该对象在常量池中的引用
如果没有,则会在常量池中创建一个新对象,再返回引用。

2、new String()

String s1 = new String("bbb");  // s1指向内存中的对象引用

这种方式会保证字符串常量池中都有这个对象,没有就创建,最后返回堆内存中的对象引用。

3、intern方法

String s1 = new String("ccc");   
String s2 = s1.intern();
System.out.println(s1 == s2);  //false

String中的intern方法是一个 native 的方法,当调用 intern方法时,
如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串
否则,
jdk1.6:将 s1 复制到字符串常量池里,返回字符串常量池中引用给s2。
jdk1.7:将intern返回的引用指向当前字符串 s1 ,此时字符串常量池中存放的是指向堆中的“hello”对象指针
jdk1.7字符串常量池中它存储的是指向堆中 String 对象的引用,而不是对象本身

字符串常量池位置
Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池。
Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到
Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在

字符串连接操作符 + 的作用

1、基本作用:字符串连接

  • + 将两个 String 对象的内容连接起来,产生一个新的 String 对象.

2、编译器优化:转换为 StringBuilder

对于非编译期常量的字符串连接,编译器不会在编译时直接计算出结果,而是会将 + 操作转换为 StringBuilder(或 StringBuffer)的 append() 和 toString() 调用。

  • new StringBuilder():创建一个可变的字符串缓冲区。
  • .append(…):依次追加内容(不会创建中间字符串对象)。
  • .toString():最后创建一个新的 String 对象,内容为 “hello”。

3、+ 在循环中的性能

String result = "";
for (int i = 0; i < 1000; i++) {
    result = result + i;    // 每次循环都会创建新的 StringBuilder 对象
}

编译器会优化单次 + 为 StringBuilder,但在循环中这样写,每次迭代都会新建一个 StringBuilder 对象,性能依然很差。正确做法是手动在循环外使用 StringBuilder:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

String 为什么是不可变的?
String 在 Java 中被设计为不可变对象,即一旦创建,其内容(内部的 char[] 或 byte[] 数组)就不能被修改。
任何对 String 的“修改”操作(如拼接、替换、截取)都会创建一个全新的 String 对象,原对象保持不变。

Java 中的 String 被设计为不可变对象(immutable),这意味着一旦创建,它的内部字符数组(或字节数组)内容就不能被修改。这个设计不是偶然,而是综合了安全性、性能、缓存、哈希码稳定性等多方面考虑的结果。

1、安全性
类加载与反射
许多 Java 核心类(如 ClassLoader、Socket、URL、SecurityManager)都使用 String 作为参数(如类名、主机名、文件路径)。
如果 String 是可变的,攻击者可以在参数传递后修改其内容,导致安全漏洞(例如,先传入一个合法的类名,通过安全检查,之后再修改成恶意类名)。
网络与数据库连接
String 常用于表示 IP 地址、数据库 URL、用户名、密码。如果可变,这些凭据可能在连接建立后被意外篡改,造成严重问题
线程安全
不可变对象天然是线程安全的,无需任何同步措施,可以在多个线程间自由共享,不会被并发修改损坏。
2、缓存与性能优化
字符串常量池(StringTable)
JVM 为了节省内存,将字符串字面量(以及通过 intern() 显式添加的字符串)存储在常量池中。
如果 String 是可变的,常量池中的字符串被一个线程修改后,会影响所有引用该字符串的地方,导致不可预测的行为。
不可变性保证了常量池中的字符串可以被安全地共享和重用。
哈希码的稳定性
String 类缓存了哈希码(hash 字段),计算一次后存储起来,后续直接使用。
如果 String 可变,哈希码会随着内容改变而失效,导致 HashMap / HashSet 中的键失效(无法正确找回对象)。
不可变性保证了哈希码始终与内容一致,提高哈希容器性能。

字符串操作返回新对象
所有看似“修改”字符串的方法(substring、replace、concat、toUpperCase 等)都返回一个新的 String 对象,原对象不变。
这种设计使代码行为可预测,避免了因副作用导致的难以追踪的 bug。
并可作为 HashMap 的键:不可变对象(包括 String)作为 Map 的键时非常安全,不会因为键被修改而丢失映射关系。

实现层面:
1:final 类与 private final 字符数组
String 类被声明为 final,防止被继承破坏不可变性。
内部字符数组(或 JDK 9+ 的字节数组)是 private final 的,不提供任何修改数组内容的公共方法。
2:没有 setter 方法
String 没有提供任何修改内容的方法(如 setCharAt),唯一的“修改”途径是反射绕过,但这违反了 API 契约。

总结:
String 被设计为不可变,主要是为了安全(防止参数篡改、保证线程安全)、缓存(常量池复用、哈希码缓存)以及作为 HashMap 键的可靠性,同时通过提供 StringBuilder 来弥补性能损失。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐