【Java原理探索】带你攻克String类创建的难点分析 | Java开发实战

简介: 【Java原理探索】带你攻克String类创建的难点分析 | Java开发实战

字符串常量池引入


String是一个引用类型,这意味着String类型的实例化与其它对象一样,相较于基本数据类型,时间和空间的消耗都是较大的,但是由于String的使用频率非常高,JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化,引入了字符串常量池。



字符串创建过程


  • 每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用


  • 如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串


  • .class文件中的常量池将包含String字面量,在jvm进行类装载过程中,class文件中的常量池将被载入内存,此时便形成了所谓的字符串常量池。




整体来说String对象的初始化分为两种


初始化方式将会影响对象内存分配的方式,


字面量初始化的形式创建字符串


public class ImmutableStrings{
    public static void main(String[] args)
    {
        String one = "someString"; // 1
        String two = "someString"; // 2
        System.out.println(one.equals(two));  // String 对象是否相同内容
        System.out.println(one == two);  // String 对象是否相同的引用
    }
}
// Output
true
true
复制代码

执行完上面的第一句代码之后,会在堆上创建一个String 对象,并把String 对象的引用存放到字符串常量池中,并把引用返回给 one,那当第二句代码执行时,字符串常量池已经有对应内容的引用了,直接返回对象引用给 two。one.equals(two) / one == two 都为true。 图形化如下所示:

image.png



字符串拼接的初始化场景


String s = "a"+"a";
String s2 = "aa";
System.out.println(s == s2); // true
复制代码

应该思考为什么会输出true,通过反编译可知jvm直接将上面的"a"+"a"在编译阶段直接变成了"aa"。

image.png


String s = "a";
String s1 = s + "a";
String s2 = "aa";
System.out.println(s1 == s2); // false
复制代码

上面这一段输出false,同样通过反编译


image.png


可以看出汇编指令明显比上面的长了许多,然后我们逐个分析s1的产生过程


  1. 首先jvm会先生成一个StringBuilder对象

image.png

2.然后会添加s和"a",这里我们可以看出第一次添加的时候需要通过ldc出栈解析了字符串s的值,然后添加到StringBuilder对象中。image.png

3.最后调用StringBuilder对象的toString方法返回一个新的字符串对象。image.png

4.StringBuilder的toString方法如下,所以上面s1==s2为false。image.png




通过上面的分析


  • 我们可以知道当String s1 = "a" + "a"时在编译阶段由于可以直接确定s1的值,所以在编译阶段直接将s1的值赋值为aa
  • String s1 = s+ "a"在编译阶段中由于不知道s的内容(在编译阶段jvm不会知道一个对象的内容),所以需要运行期间来解析s并且通过StringBuilder进行优化来将它们相加。  


所以我们在平时写代码的时候对于字符串拼接用StringBuilder来拼接,因为String类型相加底层用的StringBuilder,而每一次String相加都会生成一个对象,使用StringBuilder可以节约内存,避免内存溢出



new创建字符串


众所周知String s = new String("a")将会在生成一个String对象,字符串a会不会加入到常量池中呢?我们对String s = new String("a")也进行反编译如下:


image.png


  • 通过对上面进行反编译可以看到使用new创建对象的时候执行了ldc这个指令,ldc指令的意思是操作字符串常量池,如果有直接拉取下来,如果没有就创建一个对象在常量池中
  • 通过反编译我们可以看出使用new String()创建对象的时候我们访问了字符串常量池的,那么是不是创建s对象的时候在常量池也创建了一个"a"呢


可以理解为


String variable = "a"; // variable为匿名变量
String s = new String(variable);
复制代码

也就是说在第二种初始化中是包含了第一种初始化的,首先进行的是以字面量的形式创建匿名变量,具体流程与第一种方式初始化一致,然后new操作会在堆上创建s指向的String对象,也就是说第二种方式初始化实际上会创建两个String对象,一个存放在字符串常量池,一个存放在堆中;


实际案例分析


public class ImmutableStrings
{
    public static void main(String[] args)
    {
        String one = "someString";
        String two = new String("someString");
        System.out.println(one.equals(two));
        System.out.println(one == two);
    }
}
// Output
true
false


image.png

带着这个疑问我们看看String 的构造函数

image.png



  • 这个构造函数只是将"a"的value和hash赋值给了新创建的对象,而value是char[]类型的数组,hash也是int类型,这两个都不可能对字符串常量池访问,所以真正的原因只可能是传入的"a"是从字符串常量池中获取的,所以我们在new String()的时候有可能会生成两个对象。
  • 所以对于new String("a")可能会生成两个对象,一个是字符串类型对象存放在堆中,另外一个就是字符串常量池对象,当然如果a 以前在字符串常量池中存在那么将不会创建字符串常量池对象。



注意:new String()本身不会在字符串常量池中创建相应的对象,new String("a")会生成两个对象的原因是因为"a"在加载new String("a");这行代码所存在的类的时候将其放入字符串常量池中的。比如"int"就是java在Integer类的初始化阶段时候在解析 public static final Class TYPE = (Class) Class.getPrimitiveClass("int");这行代码的时候放入到字符串常量池中的,具体可以根据工具进行查看 =》工具可以使用GDB


String s = new String(new char[]{'a','b','c'});

image.png


可以看出当使用char[]数组创建对象的时候并没有访问常量池,通过上面我们可以得出只要在代码中出现"a","ab"这种直接告诉我们这是什么的字符串才会在常量池中创建相应的对象


可以得出以下结论:


字面量形式的String对象初始化都会被加入字符串常量池;此时当内容一致时,多个引用会同时指向同一对象,这也是为什么String会被设计成immutability(不变性),防止当一个引用更改对象的内容时,其它引用被迫更改。


使用new操作创建的String对象,一定会在堆上创建对象,但是如果涉及到字面量初始化,则会创建两个对象,分别存放在字符串常量池与堆中。


  1. 在直接使用双引号"" 声明字符串的时候,java 都会去常量池通过equal找有没有相同的字符串,如果有,则将常量池的引用返回给变量,如果没有 回在常量池中创建一个对象,然后返回这个对象的引用
  2. 使用new 关键字创建,例如 String a = new String("a"),这里首先会去常量池对比有没有"a",没有则会创建,其次 new 一定会在堆里面创建一个新对象 并返回该对象的引用
  3. 使用+ 运算符,此处有大致有三种情况,


String str = "ab"+"cd"; 在常量池上创建常量ab, cd ,abcd 返回 abcd【着重了解abcd在常量池上】

String str = new String("ab") + new String("cd"); 在堆上创建对象ab、cd和 abcd,在常量池上创建常量ab和cd ,常量池上不会创建 abcd【着重了解abcd 不会在常量池上】


还有混合使用的就不在一一说明 如 String str = "ab" + new String("cd"); 或者 String strAb = "ab"; String str = strAb + new String("cd") 等情况


验证运算符
String s2 = new String("a") +"b"; //s2 指向堆 里面ab 的地址,并且常量池不存在 ab
String s3 = s2.intern(); // 由于常量池并没有ab 因此会把 s2 的堆地址引用 放到常量池
System.out.println(s2 == s3);// 同时指向 ab 堆里面的地址 固为true
复制代码



验证运算符与字符串混用


区别仅仅就是多定义了一个String s1 ="ab",

String s1 = "ab"; //s1指向 ab常量池地址
String s2 = new String("a") +"b"; //s2 指向 堆 里面ab 的地址,常量池已存在 ab
String s3 = s2.intern(); // 由于常量池存在 ab 因为会把 ab 常亮池的引用返回
System.out.println(s2 == s3);// s2 指向堆 s3地址字符常量池 固为false
复制代码



来一个特殊字符串


与代码3 一样仅仅是字符串替换成了"java" 返回结果为false 其实此处就是因为jvm虚拟机在其他类【Version.class】先定义了放入了常量池,其实原理就和代码4一样 先把ab 放入了常量池 了解了原理就可以举一反三

String s2 = new String("ja") +"va"; //s2 指向 堆 里面java 的地址,常量池已存在 java【原因查看Version.class】
String s3 = s2.intern();// 由于常量池存在 java 因为会把 java 常亮池的引用返回
System.out.println(s2 == s3);// s2 指向堆 s3地址字符常量池 固为false


image.png




运行时常量的包装类


8种基本数据类型都有自己的包装类,在包装类对象创建的实话就会消耗资源,因此 java 对 其中5种(Byte,Short,Integer,Long,Character,Boolean)包装类实现了常量池技术,默认创建了数值(-128 ,127)的相应类型的缓存数据,但是超出了此范围依然会去创建新的对象。两种浮点数类型的包装类 (Float,Double) 并没有实现常量池技术










相关文章
|
18天前
|
存储 JavaScript Java
Java 中的 String Pool 简介
本文介绍了 Java 中 String 对象及其存储机制 String Pool 的基本概念,包括字符串引用、构造方法中的内存分配、字符串文字与对象的区别、手工引用、垃圾清理、性能优化,以及 Java 9 中的压缩字符串特性。文章详细解析了 String 对象的初始化、内存使用及优化方法,帮助开发者更好地理解和使用 Java 中的字符串。
Java 中的 String Pool 简介
|
5天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
4天前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
18 6
|
24天前
|
缓存 安全 Java
java 为什么 String 在 java 中是不可变的?
本文探讨了Java中String为何设计为不可变类型,从字符串池的高效利用、哈希码缓存、支持其他对象的安全使用、增强安全性以及线程安全等方面阐述了不可变性的优势。文中还通过具体代码示例解释了这些优点的实际应用。
java 为什么 String 在 java 中是不可变的?
|
18天前
|
存储 Java
Java 11 的String是如何优化存储的?
本文介绍了Java中字符串存储优化的原理和实现。通过判断字符串是否全为拉丁字符,使用`byte`代替`char`存储,以节省空间。具体实现涉及`compress`和`toBytes`方法,前者用于尝试压缩字符串,后者则按常规方式存储。代码示例展示了如何根据配置决定使用哪种存储方式。
|
28天前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
1月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
54 8
|
3天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
5天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
5天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。