【JDK1.8】String对象的内存分配

本文深入探讨了JDK1.8环境下String对象的内存分配与intern方法的工作原理。通过实例分析,详细解释了字符串字面量、非纯字面量在不同情况下的内存位置与引用关系,以及编译器优化对内存分配的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于jvm的内存分布相信大家已经不陌生了,在此不加赘述,直接来看String在创建过程中的内存分配吧。本文所有代码均运行于JDK1.8

java version “1.8.0_172”
Java™ SE Runtime Environment (build 1.8.0_172-b11)
Java HotSpot™ 64-Bit Server VM (build 25.172-b11, mixed mode)

1. intern方法的作用

		String s1 = new StringBuffer().append("123").append("123").toString();
        String s2 = s1.intern();
        System.out.println("s1 == s2 : " + (s1 == s2));

        String s3 = new StringBuffer().append("456").append("456").toString();
        String s4 = "456456";
        System.out.println("s3 == s4 : " + (s3 == s4));
        System.out.println("s3 == s3.intern : " + (s3 == s3.intern()));

s1 == s2 : true
s3 == s4 : false
s3 == s3.intern : false

当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中 (不会创建新的对象而是在池中保存指向堆中对象的引用 --JDK1.8 ,并返回此对象的引用。

这句话什么意思呢?就是说调用一个String对象的intern()方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中),如果没有,则将该对象引用添加到池中,并返回池中的引用。

案例分析 1

分析上述代码:

  1. String s1 = new StringBuffer().append("123").append("123").toString();
    在池中创建了池:"123",在堆中创建了堆:"123123",s1指向堆:"123123"
  2. String s2 = s1.intern();
    s2 指向了堆中的堆:"123123",同时将引用置于池中
  3. 所以 s1 == s2 为 true

  1. String s3 = new StringBuffer().append("456").append("456").toString();
    在池中创建了池:"456",在堆中创建了堆:"456456",s3指向堆:"456456"
  2. String s4 = "456456";
    在池中创建了池:"456456",s4 指向了池中的池:"456456"
  3. 所以 s3 == s4 为 false
  4. System.out.println("s3 == s3.intern : " + (s3 == s3.intern()));
    s3 指向 堆:"456456" 此时,由于s4的创建,池中有了和s3内容相同的String对象
    s3.intern 指向 池:"456456" 【与 [2.]内容相对】

		String s1 = new StringBuffer().append("123").append("123").toString();
        String s2 = s1.intern();
        String s3 = "123123";
        System.out.println("s1 == s2 : " + (s1 == s2));
        System.out.println("s1 == s3 : " + (s1 == s3));
        System.out.println("s2 == s3 : " + (s2 == s3));

s1 == s2 : true
s1 == s3 : true
s2 == s3 : true

  1. 在上述代码中由于s1.intern();的执行,池中保存的是堆:"123123"的引用,
    s3在创建时发现池中有相同内容String的引用,
    所以 s1 = s2 = s3 = &堆:"123123"

2. 字符串相加

		String s5 = "789" + "789" + new String("789");
        String s6 = "789789789";
        System.out.println("s5 == s6 : " + (s5 == s6));

        String s7 = "789" + "789";
        String s8 = s7.intern();
        System.out.println("s7 == s8 : " + (s7 == s8));

		String s9 = "aaa" + new String("aaa") + "aaa";
        String s10 = "aaaaaaaaa";
        System.out.println("s9 == s10 : " + (s9 == s10));

        String s11 = "bbb" + "bbb";
        String s12 = "bbbbbb";
        System.out.println("s11 == s12 : " + (s11 == s12));

        String s13 = "ccc";
        String s14 = s13 + "ccc";
        String s15 = "cccccc";
        System.out.println("s14 == s15 : " + (s14 == s15));

s5 == s6 : false
s7 == s8 : true
s9 == s10 : false
s11 == s12 : true
s14 == s15 : false

案例分析 2

分析上述代码,

  1. s5 == s6 : false 非纯字面常量相加,最终产生的对象位于堆中
  2. s7 == s8 : true 纯字面常量相加,最终产生的对象位于池中
  3. s9 == s10 : false 非纯字面常量相加,最终产生的对象位于堆中, 与顺序无关
  4. s11 == s12 : true 纯字面常量相加,最终产生的对象位于池中,和直接书写没有区别
  5. s14 == s15 : false 非纯字面常量相加,最终产生的对象位于堆中,与初始化方式无关(无论参数原本位于池中还是堆中)

 		String s2 = new StringBuffer().append("789").append("789").toString();
        System.out.println("s2 == s2.intern : " + (s2 == s2.intern()));
        // s2 == s2.intern : true
  1. 在堆中创建的对象先调用intern会将引用置于池中(实际上经过编译器优化,是直接指向堆)
		String s1 = "789" + "789" + new String("789");
        String s2 = new StringBuffer().append("789").append("789").toString();
        System.out.println("s2 == s2.intern : " + (s2 == s2.intern()));
        // s2 == s2.intern : false
  1. 在非纯字面量相加的表达式中,连续纯字面量相加部分会被优化为单个字面量,并存于池中
        String s1 = new String("789") + "789" + "789";
        String s2 = new StringBuffer().append("789").append("789").toString();
        System.out.println("s2 == s2.intern : " + (s2 == s2.intern()));
        // s2 == s2.intern : false
  1. 以上结论显然与顺序无关(实质上非纯字面量相加过程中各部分会被优化为类似new StringBuffer().append("789").append("789789").toString();的形式)
		String s16 = "ddd" + "ddd" + "ddd";
        String s17 = new StringBuffer().append("ddd").append("ddd").toString();
        String s18 = s17.intern();
        System.out.println("s17 == s18 : " + (s17 == s18));// 没有创建中间量
		// s17 == s18 : true
        String s19 = "eee" + "eee";
        String s20 = new StringBuffer().append("eee").append("eee").toString();
        String s21 = s20.intern();
        System.out.println("s20 == s21 : " + (s20 == s21));       
		// s20 == s21 : false
  1. 连续字面常量相加不会产生中间量(池中没有),只有最终结果(置于池)

小结 –JDK1.8

  1. 只有在使用"字符串字面常量"的情况下才会自动在池中创建对象[常量];
  2. 使用new、toString等方式创建的对象均处于堆中(直接使用其内部字符序列,不必求助池中常量),不会主动进行常量池的检查
  3. 使用intern方法会根据当前池中是否有相同内容对象决定返回引用,若池中无相应对象则将堆中对象的引用置入池(实际上经过编译器优化,返回值直接指向堆),有相应对象则返回对应常量 【1.8之前的情况有所不同,未测试】
  4. 在使用纯"字符串字面常量"的连加表达式中只有最终结果会被在池中创建,中间结果不会被创建(实际上应当被优化成了单一字量)
  5. 非纯字面常量相加,各部分会被优化成类似StringBuffer().append(某字符串对象).append("某字符串对象).append(某字符串对象).toString();的形式,置于堆中,不会主动进行常量池的检查

参考文章

JDK1.8源码(三)——java.lang.String 类
java基础篇之7————String常用类及jdk1.8中intern方法
关于JDK1.7和JDK1.8的内存分配(String)

### JDK 1.8 中 JVM 的内存模型详解 JDK 1.8 对 JVM 内存模型进行了重要调整,特别是针对方法区的设计。以下是 JDK 1.8 中 JVM 内存模型的主要组成部分及其功能描述: #### 1. **程序计数器 (Program Counter Register)** 程序计数器是一块较小的内存区域,用于记录当前线程所执行的字节码指令的位置。每条线程都有独立的程序计数器,互不影响。 #### 2. **Java 虚拟机栈 (Java Virtual Machine Stacks)** 虚拟机栈是线程私有的,生命周期与线程相同。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、返回地址等信息。如果线程请求的栈深度大于允许的最大深度,则会抛出 `StackOverflowError`;如果虚拟机栈可以动态扩展,而扩展失败则会抛出 `OutOfMemoryError`[^4]。 #### 3. **本地方法栈 (Native Method Stack)** 本地方法栈类似于 Java 虚拟机栈,但它服务于 Native 方法。同样地,如果栈深度超过最大限制,可能会抛出 `StackOverflowError` 或 `OutOfMemoryError`。 #### 4. **Java 堆 (Heap)** Java 堆是 JVM 所管理的内存中最大的一块,被所有线程共享。它是垃圾收集器(GC)管理的主要区域,几乎所有的对象实例都在这里分配。堆的空间大小可以通过 `-Xmx` 和 `-Xms` 参数设置。如果堆中的可用内存不足以分配新对象,并且堆无法进一步扩展,则会抛出 `OutOfMemoryError` 异常[^5]。 #### 5. **方法区 (Method Area)** 方法区是一个线程共享的内存区域,主要用于存储已被虚拟机加载的类信息、常量、静态变量以及即时编译后的代码缓存等数据。在 JDK 1.8 及之后版本中,方法区的概念由元空间(MetaSpace)实现,不再依赖于永久代(Permanent Generation)。 - **元空间的特点**: - 元空间位于本地内存(Native Memory)中,而非 JVM 的堆中。 - 它的大小受操作系统本身的内存限制影响,而不是通过 JVM 参数严格限定。 - 如果物理内存不足或者元空间耗尽,仍然可能抛出 `OutOfMemoryError` 异常。 #### 6. **运行时常量池 (Runtime Constant Pool)** 运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。这些内容会在类加载后进入方法区的运行时常量池。由于运行时常量池属于方法区的一部分,因此它的容量也受到方法区配置的影响。如果常量池过大导致方法区内存不足,也可能引发 `OutOfMemoryError`。 --- ### 示例代码:触发 OutOfMemoryError 以下代码展示了如何通过不断创建字符串对象来模拟 `OutOfMemoryError` 错误场景: ```java public class OOMExample { public static void main(String[] args) { List<String> list = new ArrayList<>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } } ``` 上述代码中,`String.intern()` 将字符串加入到运行时常量池中,可能导致方法区或元空间耗尽从而抛出异常。 --- ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值