Java性能优化(二):Java基础-String对象及其性能优化

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 在深入探讨了String字符串的性能优化后,我们认识到优化字符串处理对提升系统整体性能的重要性。Java在版本迭代中,通过精心调整成员变量和内存管理机制,不断对String对象进行优化,以更高效地使用内存资源。String对象的不可变性是Java语言设计中的一个关键特性,它不仅确保了字符串的安全性,也为字符串常量池的实现提供了基础。通过减少相同值的字符串对象的重复创建,常量池有效地节约了内存空间。然而,不可变性也带来了挑战。在处理长字符串拼接时,我们需要显式使用类来避免性能下降。
  • 作者简介:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名

  • ❤️觉得文章还不错的话欢迎大家点赞👍➕收藏⭐️➕评论,💬支持博主,记得点个大大的关注,持续更新🤞
    ————————————————-

    引言

    在Java编程中,String对象无疑是我们日常工作中使用最为频繁的数据类型之一。然而,其性能问题却往往被开发者所忽视。由于String对象在内存中占用空间较大,因此高效地使用字符串对于提升系统整体性能至关重要。本文将从String对象的实现、特性以及实际使用中的优化三个方面入手,深入探讨其性能优化的方法。

String对象的实现与特性

在这里插入图片描述

Java 6及以前版本

在Java 6及以前的版本中,String对象是对char数组进行了封装实现的对象。它主要包括四个成员变量:char数组、偏移量offset、字符数量count、哈希值hash。通过offsetcount两个属性,String对象能够高效地定位char[]数组,从而快速获取字符串。这种设计有助于实现字符串的共享,减少内存空间的占用,但也可能导致内存泄漏问题。

Java 7至Java 8版本

从Java 7版本开始,Java对String类做了一些调整。在String类中,不再包含offsetcount两个变量。这样的改动使得String对象占用的内存稍微减少,同时,String.substring方法也不再共享原始的char[]数组,从而解决了使用该方法可能导致的内存泄漏问题。

Java 9及以后版本

在Java 9及以后的版本中,工程师们对String类进行了进一步的优化。他们将char[]字段改为了byte[]字段,并新增了一个名为coder的属性。这个新属性用于标识字符串的编码格式。

之所以进行这样的修改,是因为在存储单字节编码内的字符时,使用char数组(每个字符占2个字节)会显得非常浪费。因此,JDK 1.9中的String类采用了占1个字节的byte数组来存放字符串,以节约内存空间。而coder属性则用于在计算字符串长度或使用indexOf()函数时,判断如何计算字符串长度。coder属性有两个可能的值:0代表Latin-1(单字节编码),1代表UTF-16。

实际使用中的优化

字符串创建与比较

在Java中,字符串的创建和比较是常见的操作。然而,不正确的创建和比较方式可能会导致性能问题。例如,通过new String("abc")方式创建的字符串与通过字符串常量池(String Pool)创建的字符串(如"abc")在内存中的表示是不同的。因此,在比较两个字符串是否相等时,应该使用equals()方法而不是==运算符。

此外,intern()方法也是一个值得注意的方法。它可以将一个字符串添加到字符串常量池中,并返回该字符串在常量池中的引用。如果字符串已经存在于常量池中,则直接返回该引用。这个方法在某些场景下可以用于优化字符串的使用。

面试题解析

以下是一个常见的面试题,用于考察面试者对字符串的理解:

String str1 = "abc";
String str2 = new String("abc");
String str3 = str2.intern();
assertSame(str1 == str2); // 预期为false
assertSame(str2 == str3); // 预期为false
assertSame(str1 == str3); // 预期为true(在Java 7及以上版本)

在这个例子中,str1是通过字符串常量池创建的,而str2是通过new关键字在堆内存中创建的。因此,str1str2在内存中的引用是不同的,所以str1 == str2的结果为false。而str3是通过调用str2.intern()方法得到的,由于字符串"abc"已经存在于字符串常量池中,所以str3实际上是指向常量池中"abc"的引用,因此str1 == str3的结果为true(在Java 7及以上版本)。

深入解析Java中String对象的不可变性

当我们探讨Java中的String对象时,一个不可忽视的特性就是其不可变性(immutable)。这种特性使得String对象一旦创建,其内容就无法被修改。了解String的不可变性对于正确使用Java字符串和避免常见的性能问题至关重要。

String对象的不可变性

在Java中,String类被final关键字修饰,意味着这个类不能被继承。同时,String对象内部用于存储字符的char数组也被声明为finalprivate,这确保了String对象的内容在创建后无法被修改。

不可变性的好处

  1. 安全性:由于String对象是不可变的,它们可以被安全地共享而无需担心被其他代码段意外修改。这提高了代码的安全性和可维护性。

  2. 缓存哈希值:由于String对象的内容不会改变,因此其哈希值(hashCode()方法返回的值)在对象被创建时就可以被计算并缓存起来。这提高了String对象在哈希表(如HashMap)中的性能。

  3. 字符串常量池:Java提供了字符串常量池(String Constant Pool),用于存储字符串字面量。当使用字面量创建String对象时(如String str = "abc";),JVM会首先检查常量池中是否已存在相同内容的字符串。如果存在,则返回该字符串的引用;否则,在常量池中创建一个新的字符串对象。这种机制减少了不必要的对象创建,提高了内存使用效率。

  4. 线程安全:由于String对象是不可变的,因此它们在多线程环境中是安全的。无需额外的同步机制即可在多个线程之间安全地共享String对象。

对象与对象引用的区别

在Java中,我们经常混淆对象和对象引用。当我们说String str = "hello";时,str实际上是一个指向String对象的引用,而不是对象本身。对象在内存中占用一块地址空间,而str则是这个地址的引用。

当我们执行str = "world";时,并没有修改原来的"hello"对象,而是创建了一个新的"world"对象,并将str的引用指向了这个新对象。原来的"hello"对象仍然存在于内存中(除非没有任何引用指向它,从而被垃圾收集器回收)。

这种机制使得Java中的字符串操作非常灵活和高效。但是,也需要注意不要在不必要的情况下频繁地创建新的字符串对象,以避免不必要的内存分配和垃圾收集开销。

小结

String对象的不可变性是Java语言中的一个重要特性,它带来了安全性、性能优化和线程安全等多方面的好处。同时,了解对象和对象引用的区别对于正确使用Java字符串至关重要。通过合理使用字符串常量池和避免不必要的字符串对象创建,我们可以进一步提高Java程序的性能和内存使用效率。

String对象的优化

了解了String对象的实现原理和特性后,我们将结合实际场景探讨如何优化String对象的使用,并指出在优化过程中需要注意的事项。

1. 如何构建超大字符串?

字符串常量的拼接

在编程中,字符串的拼接很常见。尽管String对象是不可变的,但在某些情况下,如字符串常量的拼接,编译器会进行优化。例如:

String str = "ab" + "cd" + "ef";

在编译时,上述代码会被优化为:

String str = "abcdef";

这意味着编译器在编译阶段就进行了优化,只生成了一个String对象。

字符串变量的拼接

然而,当涉及到字符串变量的拼接时,情况就不同了。以下代码示例:

String str = "abcdef";
for (int i = 0; i < 1000; i++) {
   
   
    str = str + i;
}

虽然从代码上看似简单,但实际上每次循环都会创建一个新的String对象,这会导致大量内存分配和垃圾回收,从而影响性能。为了提高性能,可以使用StringBuilderStringBuffer来构建超大字符串。

使用StringBuilder

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

使用StringBuilder可以避免不必要的内存分配和垃圾回收,显著提高性能。

使用StringBuffer(线程安全)

如果在多线程环境中需要构建字符串,则应使用StringBuffer。但请注意,由于StringBuffer是线程安全的,因此其性能通常低于StringBuilder

2. 使用String.intern()节省内存

Twitter每次发布消息状态的时候,都会产生一个地址信息,以当时Twitter用户的规模预估,服务器需要32G的内存来存储地址信息。
在某些情况下,如Twitter中存储大量重复的地址信息,可以使用String.intern()方法来节省内存。String.intern()方法返回字符串对象的规范表示形式。如果字符串常量池中已经存在一个与该字符串相等的字符串,则返回该字符串的引用;否则,将此字符串添加到常量池中,并返回此字符串的引用。

示例

public class SharedLocation {
   
   
    private String city;
    private String region;
    private String countryCode;

    public void setCity(String city) {
   
   
        this.city = city.intern();
    }

    // ... 其他setter方法 ...
}

通过在SharedLocation类中使用String.intern()方法,可以确保相同的字符串只被存储一次在字符串常量池中,从而显著减少内存的使用。但请注意,这种方法可能会降低性能,并且需要谨慎使用,以避免潜在的问题。

小结

在优化String对象的使用时,我们需要根据具体场景选择适当的方法。对于字符串常量的拼接,编译器会自动进行优化;对于字符串变量的拼接,应使用StringBuilderStringBuffer来提高性能;在需要节省内存的场景中,可以考虑使用String.intern()方法。但请务必注意每种方法的优缺点,并根据实际情况进行选择。

如何使用String.intern节省内存?

讲完了构建字符串的优化后,我们再来探讨下如何通过String.intern()方法来节省内存。在实际应用中,尤其是处理大量重复字符串数据时,内存使用是一个需要考虑的重要因素。Twitter这样的社交平台就是一个很好的例子,其中用户的地址信息可能包含大量重复的城市、省份和国家等字符串。

案例背景

Twitter每次发布消息时,都会记录用户的地址信息。考虑到Twitter庞大的用户规模,地址信息的存储可能会占用大量的内存。为了优化内存使用,Twitter的工程师们采取了多种策略,其中之一就是使用String.intern()

使用String.intern()

String.intern()方法用于将字符串添加到Java的字符串常量池中,并返回该字符串在常量池中的引用。如果常量池中已经存在具有相同内容的字符串,则intern()方法会直接返回该字符串的引用,而不是创建一个新的字符串对象。这有助于减少内存中的重复字符串对象,从而节省内存。

示例代码

首先,我们假设有一个MessageInfo类,它包含了地址信息:

public class MessageInfo {
   
   
    private String city;
    private String region;
    private String countryCode;
    // ... 其他字段和getter/setter方法 ...
}

public class SharedLocation {
   
   
    private String city;
    private String region;
    private String countryCode;

    public void setCity(String city) {
   
   
        this.city = city.intern();
    }

    public void setRegion(String region) {
   
   
        this.region = region.intern();
    }

    public void setCountryCode(String countryCode) {
   
   
        this.countryCode = countryCode.intern();
    }
    // ... 其他setter方法 ...
}

public class Location {
   
   
    private SharedLocation sharedLocation;
    private double longitude;
    private double latitude;

    // ... 构造器、getter/setter方法 ...
}

在上面的代码中,我们在SharedLocation类的setter方法中调用了intern()方法。这样,当设置城市、省份或国家代码时,如果常量池中已经存在具有相同内容的字符串,就会直接使用该字符串的引用,从而避免了重复创建字符串对象。

通过优化,数据存储大小减到了20G左右。但对于内存存储这个数据来说,依然很大,怎么办呢?

这个案例来自一位Twitter工程师在QCon全球软件开发大会上的演讲,他们想到的解决方法,就是使用String.intern来节省内存空间,从而优化String对象的存储。

具体做法就是,在每次赋值的时候使用String的intern方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的地址信息存储大小从20G降到几百兆。

SharedLocation sharedLocation = new SharedLocation();

sharedLocation.setCity(messageInfo.getCity().intern());        sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());

Location location = new Location();
location.set(sharedLocation);
location.set(messageInfo.getLongitude());
location.set(messageInfo.getLatitude());

为了更好地理解,我们再来通过一个简单的例子,回顾下其中的原理:

String a =new String("abc").intern();
String b = new String("abc").intern();

if(a==b) {
   
   
    System.out.print("a==b");
}

输出结果:

a==b

在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,String对象中的char数组将会引用常量池中的char数组,并返回堆内存对象引用。

如果调用intern方法,会去查看字符串常量池中是否有等于该对象的字符串的引用,如果没有,在JDK1.6版本中会复制堆中的字符串到常量池中,并返回该字符串引用,堆内存中原有的字符串由于没有引用指向它,将会通过垃圾回收器回收。

在JDK1.7版本以后,由于常量池已经合并到了堆中,所以不会再复制具体字符串了,只是会把首次遇到的字符串的引用添加到常量池中;如果有,就返回常量池中的字符串引用。

了解了原理,我们再一起看下上边的例子。

在一开始字符串"abc"会在加载类时,在常量池中创建一个字符串对象。

创建a变量时,调用new Sting()会在堆内存中创建一个String对象,String对象中的char数组将会引用常量池中字符串。在调用intern方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

创建b变量时,调用new Sting()会在堆内存中创建一个String对象,String对象中的char数组将会引用常量池中字符串。在调用intern方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。

而在堆内存中的两个对象,由于没有引用指向它,将会被垃圾回收。所以a和b引用的是同一个对象。

如果在运行时,创建字符串对象,将会直接在堆内存中创建,不会在常量池中创建。所以动态创建的字符串对象,调用intern方法,在JDK1.6版本中会去常量池中创建运行时常量以及返回字符串引用,在JDK1.7版本之后,会将堆中的字符串常量的引用放入到常量池中,当其它堆中的字符串对象通过intern方法获取字符串对象引用时,则会去常量池中判断是否有相同值的字符串的引用,此时有,则返回该常量池中字符串引用,跟之前的字符串指向同一地址的字符串对象。

以一张图来总结String字符串的创建分配内存地址情况:
在这里插入图片描述

使用intern方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个HashTable的实现方式,HashTable存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。

原理回顾

  • 在Java中,字符串常量(即直接通过字面量创建的字符串)默认会被放入字符串常量池中。
  • 当我们使用new String("abc")创建一个新的字符串对象时,这个对象会被创建在堆内存中,并且如果此时常量池中还没有"abc"这个字符串,则会在常量池中创建一个新的字符串对象(在JDK 1.6及之前版本)。
  • 调用intern()方法时,JVM会检查常量池中是否已经存在与当前字符串内容相同的字符串。如果存在,则返回该字符串的引用;如果不存在,则在常量池中创建一个新的字符串对象(在JDK 1.6及之前版本),并返回该字符串的引用。
  • 在JDK 1.7及之后的版本中,字符串常量池被移动到了堆中,因此intern()方法的行为稍有不同。但核心思想仍然是检查常量池中是否存在相同内容的字符串,并返回其引用。

注意事项

  • String.intern()方法虽然可以节省内存,但也会带来一定的性能开销,因为每次调用都需要检查常量池。因此,在决定是否使用intern()方法时,需要权衡内存节省和性能开销之间的利弊。
  • String.intern()方法返回的是字符串在常量池中的引用,这意味着它返回的字符串是不可变的。如果你需要修改字符串的内容,那么你应该考虑使用StringBuilderStringBuffer
  • 在多线程环境中使用String.intern()时需要特别小心,因为字符串常量池是全局共享的,并且intern()方法的调用是线程安全的。但是,如果多个线程同时调用intern()方法并期望获得相同的字符串引用,那么它们可能会因为竞态条件而得到不同的结果。因此,在多线程环境中使用String.intern()时需要谨慎处理线程同步问题。

如何使用字符串的分割方法?

最后我想跟你聊聊字符串的分割,这种方法在编码中也很最常见。Split()方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致CPU居高不下。

所以我们应该慎重使用Split()方法,我们可以用String.indexOf()方法代替Split()方法完成字符串的分割。如果实在无法满足需求,你就在使用Split()方法时,对回溯问题加以重视就可以了。

**总结:

在深入探讨了String字符串的性能优化后,我们认识到优化字符串处理对提升系统整体性能的重要性。Java在版本迭代中,通过精心调整成员变量和内存管理机制,不断对String对象进行优化,以更高效地使用内存资源。

String对象的不可变性是Java语言设计中的一个关键特性,它不仅确保了字符串的安全性,也为字符串常量池的实现提供了基础。通过减少相同值的字符串对象的重复创建,常量池有效地节约了内存空间。

然而,不可变性也带来了挑战。在处理长字符串拼接时,我们需要显式使用StringBuilder类来避免性能下降。StringBuilder通过其可变性,允许我们在一个对象内多次进行字符串的追加操作,从而显著提高拼接性能。

除了使用StringBuilder,我们还可以通过intern方法进一步优化字符串使用。这个方法允许我们将一个字符串引用到常量池中的现有对象,如果常量池中已经存在相同值的对象,则直接返回该对象的引用,从而避免了重复对象的创建。

在此,我想分享一个个人见解:在软件开发中,细节决定成败。对于字符串这样看似简单的数据类型,如果我们对其了解不够深入,使用不够恰当,很可能引发意想不到的问题。例如,我曾在实际工作中因不当地使用正则表达式进行字符串匹配而导致并发瓶颈,这就是一个典型的字符串性能问题。

欢迎一键三连(关注+点赞+收藏),技术的路上一起加油!!!代码改变世界

  • 关于我:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名),回复暗号,更能获取学习秘籍和书籍等

  • ---⬇️欢迎关注下面的公众号:进朱者赤,认识不一样的技术人。⬇️---

相关文章
|
22小时前
|
Java API 索引
java中String类常用API
java中String类常用API
|
1天前
|
存储 设计模式 Java
java实习生面试题_java基础面试_java面试题2018及答案_java面试题库
java实习生面试题_java基础面试_java面试题2018及答案_java面试题库
|
1天前
|
SQL 算法 安全
java面试宝典_java基础面试_2018java面试题_2019java最新面试题
java面试宝典_java基础面试_2018java面试题_2019java最新面试题
|
3天前
|
存储 安全 Java
Java集合类是Java编程语言中用于存储和操作一组对象的工具
【6月更文挑战第19天】Java集合类,如`List`、`Set`、`Map`在`java.util`包中,提供高级数据结构。常用实现包括`ArrayList`(快速随机访问)、`LinkedList`(高效插入删除)、`HashSet`(无序不重复)、`TreeSet`(排序)、`HashMap`(键值对)和`TreeMap`(排序映射)。集合动态调整大小,支持对象引用,部分保证顺序。选择合适集合优化性能和数据组织。
8 1
|
20小时前
|
Java
“深入探讨Java中的对象拷贝:浅拷贝与深拷贝的差异与应用“
“深入探讨Java中的对象拷贝:浅拷贝与深拷贝的差异与应用“
|
1天前
|
存储 Java 程序员
java中的context对象?
java中的context对象?
|
缓存 Oracle IDE
深入分析Java反射(八)-优化反射调用性能
Java反射的API在JavaSE1.7的时候已经基本完善,但是本文编写的时候使用的是Oracle JDK11,因为JDK11对于sun包下的源码也上传了,可以直接通过IDE查看对应的源码和进行Debug。
323 0
|
1天前
|
Java 程序员
从菜鸟到大神:JAVA多线程通信的wait()、notify()、notifyAll()之旅
【6月更文挑战第21天】Java多线程核心在于wait(), notify(), notifyAll(),它们用于线程间通信与同步,确保数据一致性。wait()让线程释放锁并等待,notify()唤醒一个等待线程,notifyAll()唤醒所有线程。这些方法在解决生产者-消费者问题等场景中扮演关键角色,是程序员从新手到专家进阶的必经之路。通过学习和实践,每个程序员都能在多线程编程的挑战中成长。
|
1天前
|
缓存 安全 Java
Java线程面试题含答案
Java线程面试题含答案
|
1天前
|
Java
并发编程的艺术:Java线程与锁机制探索
【6月更文挑战第21天】**并发编程的艺术:Java线程与锁机制探索** 在多核时代,掌握并发编程至关重要。本文探讨Java中线程创建(`Thread`或`Runnable`)、线程同步(`synchronized`关键字与`Lock`接口)及线程池(`ExecutorService`)的使用。同时,警惕并发问题,如死锁和饥饿,遵循最佳实践以确保应用的高效和健壮。
8 2