开发易忽视的问题:Java String底层源码分析

String最大存储字符个数

Java 中的 String 类用于表示字符序列,其理论上的最大长度是由 JVM 的实现和可用内存资源决定的。不同版本的 JDK 在实现细节上可能会有所差异,但一般来说,String 的最大长度主要受到以下两个因素的限制:

  1. 数组的最大长度:在 Java 中,String 底层是由一个 char[] 数组实现的。根据 JVM 规范,数组的最大长度为 Integer.MAX_VALUE(即 2^31 - 1),这个值为 2147483647。
  2. 内存限制:即使理论上数组长度可以达到 Integer.MAX_VALUE,实际应用中受限于 JVM 进程可用的内存。因此,即使字符串的最大长度可以达到 2^31 - 1 个字符,在内存不足的情况下,也无法达到这个极限。

不同 JDK 版本的对比

从 JDK 1.8 到 JDK 11,String 类底层实现发生了一些变化,但对于最大字符存储能力的影响不大。

JDK 1.8

在 JDK 1.8 中,String 类是用 char[] 实现的,每个 char 占用 2 个字节(因为 Java 使用 UTF-16 编码)。因此,最大字符串长度为 Integer.MAX_VALUE 个字符。

 

java

代码解读

复制代码

public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; }

JDK 9 和更高版本

从 JDK 9 开始,String 类的内部实现改为使用 byte[] 数组来代替 char[],并引入了 "Compact Strings" 优化,这意味着如果字符串内容可以用单字节表示(如 ASCII 字符),则使用单字节存储。当不能用单字节表示时,仍然使用双字节存储。

 

java

代码解读

复制代码

public final class String implements java.io.Serializable, Comparable<String>, CharSequence { @Stable private final byte[] value; private final byte coder; // 用于指示编码类型,LATIN1 或 UTF16 }

尽管内部表示发生了变化,但最大字符串长度依然受限于 Integer.MAX_VALUE 个字符。

实际例子

尝试创建非常大的字符串时,有可能遇到 OutOfMemoryError 或者 JVM 崩溃,但这仅仅是由于内存限制,而不是字符串长度本身的限制。

整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记的【点击此处即可】即可免费获取

java

代码解读

复制代码

public class LargeStringExample { public static void main(String[] args) { try { int maxLength = Integer.MAX_VALUE; StringBuilder sb = new StringBuilder(maxLength); for (int i = 0; i < maxLength; i++) { sb.append('a'); //这一行就会崩掉 } String largeString = sb.toString(); System.out.println("Created string of length: " + largeString.length()); } catch (OutOfMemoryError e) { System.out.println("Ran out of memory!"); } } }

小结

  1. 最大长度:理论上,Java 中字符串的最大长度为 Integer.MAX_VALUE 个字符,即 2^31 - 1,约为 21 亿字符。
  2. 实现差异:不同 JDK 版本在内部实现上有差异,但这不会影响字符串能存储的最大字符数量。
  3. 实际限制:受限于系统提供的内存,实际能够存储的字符串长度通常远少于理论最大值。在实践中,超大字符串往往会导致 OutOfMemoryError

StringBuilder toString方法

StringBuilder 的 toString() 方法是将 StringBuilder 中累积的字符序列转换成一个不可变的 String 对象。理解这个方法的源码有助于深入了解其内部工作原理。

JDK 源码解析

以下是 JDK 8 中 StringBuilder 类的 toString() 方法源码:

 

java

代码解读

复制代码

@Override public String toString() { // 创建一个新的字符数组,长度为当前字符数 return new String(value, 0, count); }

在这里,我们可以看到 StringBuilder 的 toString() 方法主要做了以下几件事:

  1. 调用了 String 的构造函数,并将当前 StringBuilder 对象中的字符数组 (value) 和有效的字符数量 (count) 传递给它。
  2. new String(value, 0, count) 创建了一个新的 String 对象,该对象包含了 StringBuilder 当前存储的所有字符。

内部实现细节

为了更深入地理解这一过程,我们需要查看 String 构造函数的实现。在 JDK 8 中,对应的 String 构造函数如下:

 

java

代码解读

复制代码

public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count < 0) { throw new StringIndexOutOfBoundsException(count); } // 防止超过数组界限 if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset + count); }

关键点解析
  1. 参数校验

    • 检查 offset 是否小于 0。
    • 检查 count 是否小于 0。
    • 检查 offset + count 是否超过了原始数组的长度。

    如果这些检查中任何一项未通过,则抛出 StringIndexOutOfBoundsException 异常。

  2. 创建新数组

    • 使用 Arrays.copyOfRange 方法,从原始字符数组 value 中复制从 offset 开始、长度为 count 的字符序列,形成一个新的字符数组。
  3. 赋值

    • 将新建的字符数组赋值给 this.value,即 String 对象内部的字符数组。

JDK 8 中的 String 类

在 JDK 8 中,String 类内部通过一个 char[] 数组来存储字符数据:

 

java

代码解读

复制代码

public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; // Other fields and methods... }

当我们调用 String 类的构造方法或其他操作时,这个 value 数组是如何被初始化、处理并最终转化为字符串内容的呢?

构造方法

让我们先看一下 String 的一个典型构造方法:

 

java

代码解读

复制代码

public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }

这个构造方法直接接收一个字符数组 value,并使用 Arrays.copyOf 方法复制一份新的数组赋值给 this.value。这样保证了String 的不可变性,因为外部传入的数组的任何改变不会影响到 String 对象内部的字符数据。

核心方法:toString()

尽管 String 类本身已经表示字符串,但为了符合 Java 的对象设计模式,它依然覆盖了 Object 类的 toString() 方法。事实上,对于 String 类来说,toString() 方法返回的就是它自己:

 

java

代码解读

复制代码

@Override public String toString() { return this; }

字符转化过程

对于一个 String 实例,当我们想得到这个字符串的具体字符内容时,可以调用以下方法:

  1. charAt(int index) :

     

    java

    代码解读

    复制代码

    public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }
  2. getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) :

     

    java

    代码解读

    复制代码

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { if (srcBegin < 0) { throw new StringIndexOutOfBoundsException(srcBegin); } if (srcEnd > value.length) { throw new StringIndexOutOfBoundsException(srcEnd); } if (srcBegin > srcEnd) { throw new StringIndexOutOfBoundsException(srcEnd - srcBegin); } System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); }

这些方法都是直接或间接地操作内部的 value 数组,从而获取或复制内部的字符数据。

转化成显示形式

当我们希望将一个 String 对象输出到控制台或日志中时,Java 会隐式调用 toString() 方法。如果我们直接打印字符串对象,例如:

 

java

代码解读

复制代码

String str = "Hello, World!"; System.out.println(str);

在这种情况下,System.out.println 会调用 String.valueOf(Object) 方法,该方法实现如下:

 

java

代码解读

复制代码

public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString(); }

由于 String 类重写了 toString() 方法且返回自身,因此上述代码实际上等效于:

 

java

代码解读

复制代码

System.out.println(str.toString()); // 等价于 System.out.println(str);

JDK 9及以后的 String 实现变化

在 JDK 9 及之后的版本中,String 类的内部实现发生了一些变化。JDK 9 引入了所谓的压缩字符串(Compact Strings)机制,以提升内存效率。这个机制通过使用 byte[] 数组而不是 char[] 数组来存储字符串数据,并且根据字符串内容自动选择存储格式(LATIN1 或 UTF16)。

让我们看看 JDK 9+ 中 String 类的相关部分:

字段定义

在 JDK 9 及以后,String 类内部有以下字段:

 

java

代码解读

复制代码

private final byte[] value; private final byte coder;

  • value 是一个字节数组,用于存储字符串的数据。
  • coder 是一个字节值,指示字符串的编码(LATIN1 或 UTF16)。
构造函数示例

构造函数会根据输入的字符数组来初始化这些字段。例如:

 

java

代码解读

复制代码

public String(char[] value, int offset, int count) { // 检查并复制字符数据 byte[] val = StringUTF16.toBytes(value, offset, count); this.value = val; this.coder = UTF16; // 假设这里根据实际数据选择编码 }

toString() 方法

尽管内部实现有所不同,但 String 类的 toString() 方法仍然保持简单直接:

 

java

代码解读

复制代码

@Override public String toString() { return this; }

这与 JDK 8 的实现一致:直接返回 this,因为 String 已经是字符串的具体表示。

System.arraycopy解析

System.arraycopy 是 Java 标准库中用于高效数组复制的一个本地方法。它通过调用底层的本地操作系统函数来实现高性能的内存复制。我们可以从以下几个方面详细了解其实现原理。

方法签名

 

java

代码解读

复制代码

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

  • src:源数组。
  • srcPos:源数组中的起始位置。
  • dest:目标数组。
  • destPos:目标数组中的起始位置。
  • length:要复制的元素数量。

特点

  • 本地方法:使用 native 关键字,表示该方法在 Java 中声明,但由本地代码(通常是 C/C++)实现。
  • 高性能:由于直接调用底层系统操作,避免了循环和逐元素复制带来的开销。

本地方法实现

因为 System.arraycopy 是本地方法,所以其实现依赖于具体的 JVM(Java 虚拟机)实现。在 OpenJDK 中,这个方法大多用 C++ 实现,为了在不同的平台上提供高性能的数组复制功能。

以下是 OpenJDK 中 System.arraycopy 的一个可能实现:

在 OpenJDK 中的实现(HotSpot VM)

在 HotSpot JVM 中,System.arraycopy 的实现位于 src/hotspot/share/prims/arraycopy.cpp 文件中。下面是一个简化示例:

 

cpp

代码解读

复制代码

JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos, jobject dst, jint dst_pos, jint length)) { // 检查参数有效性,如空指针、负索引、类型兼容性等 if (src == NULL || dst == NULL) { THROW(vmSymbols::java_lang_NullPointerException()); } if (length < 0) { THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException()); } // 进一步检查数组的类型和边界条件 // 获取实际的数组指针和元素大小 // 使用 memcpy 或 memmove 等底层函数进行内存块拷贝 memmove((char*)dst + dst_pos * element_size, (char*)src + src_pos * element_size, length * element_size); return; } JVM_END

在这个实现中:

  1. 参数检查:首先检查源数组和目标数组是否为 null,长度是否为负数,其他边界条件等。如果发现任何问题,会抛出相应的异常。
  2. 类型和边界验证:确认源数组和目标数组的类型是否兼容,并确保复制范围不超出数组的边界。
  3. 获取数组指针和元素大小:计算实际的内存地址偏移量。
  4. 内存复制:使用 memmove 函数进行内存块的复制。这是一个标准的 C 库函数,用于处理重叠区域的安全内存复制。

优化与性能

System.arraycopy 的实现通常会针对不同的数据类型和平台进行优化,以确保最佳的性能。例如,对于基本数据类型的数组复制,可以使用 SIMD 指令或其他硬件加速技术。

使用示例

以下是如何在 Java 中使用 System.arraycopy 的示例代码:

 

java

代码解读

复制代码

public class ArrayCopyExample { public static void main(String[] args) { int[] srcArray = {1, 2, 3, 4, 5}; int[] destArray = new int[5]; // 执行数组复制 System.arraycopy(srcArray, 0, destArray, 0, srcArray.length); // 输出目标数组 for (int i : destArray) { System.out.print(i + " "); } // 输出结果: 1 2 3 4 5 } }

总结

  • System.arraycopy 是一个本地方法:它通过本地代码实现高效的数组复制。
  • 参数检查和边界验证:在进行实际的内存复制之前,进行必要的参数检查和类型验证。
  • 高效内存复制:通常使用底层系统的内存复制函数(如 memmove)来实现高效的数组数据移动。
  • 优化:针对不同的数据类型和平台进行专门优化,以提供最佳的性能。
  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值