Java 性能调优实战
刘超
前金山软件技术经理
59174 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 49 讲
开篇词 (1讲)
模块一 · 概述 (2讲)
结束语 (1讲)
Java 性能调优实战
15
15
1.0x
00:00/00:00
登录|注册

03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据

总结
String对象的优化
String对象的不可变性
String对象是如何实现的?
Java性能调优之字符串优化

该思维导图由 AI 生成,仅供参考

你好,我是刘超。
从第二个模块开始,我将带你学习 Java 编程的性能优化。今天我们就从最基础的 String 字符串优化讲起。
String 对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的。String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。
接下来我们就从 String 对象的实现、特性以及实际使用中的优化这三个方面入手,深入了解。
在开始之前,我想先问你一个小问题,也是我在招聘时,经常会问到面试者的一道题。虽是老生常谈了,但错误率依然很高,当然也有一些面试者答对了,但能解释清楚答案背后原理的人少之又少。问题如下:
通过三种不同的方式创建了三个对象,再依次两两匹配,每组被匹配的两个对象是否相等?代码如下:
String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
assertSame(str1==str2);
assertSame(str2==str3);
assertSame(str1==str3)
你可以先想想答案,以及这样回答的原因。希望通过今天的学习,你能拿到满分。

String 对象是如何实现的?

在 Java 语言中,Sun 公司的工程师们对 String 对象做了大量的优化,来节约内存空间,提升 String 对象在系统中的性能。一起来看看优化过程,如下图所示:
1. 在 Java6 以及之前的版本中,String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量:char 数组、偏移量 offset、字符数量 count、哈希值 hash。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了Java中String对象的实现、特性以及优化方法。首先介绍了String对象的实现方式,从Java6到Java9版本的优化过程,包括对char数组的封装、offset和count属性的变化,以及引入coder属性的原因。文章还强调了String对象的不可变性,解释了final关键字修饰的作用,以及不可变性带来的好处,如保证安全性、唯一性和实现字符串常量池。此外,文章还控诉了String对象的优化方法,特别是在构建超大字符串时,建议使用StringBuilder来提高系统性能。另外,文章还介绍了如何使用String.intern方法来节省内存空间,以及谨慎使用字符串的分割方法。总的来说,本文通过深入浅出的方式,为读者提供了关于Java中String对象优化的全面指南。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 性能调优实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(132)

  • 最新
  • 精选
  • KL3
    老师,能解释下, “String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。” 共享char数组可能导致内存泄露问题?

    作者回复: 你好 KL3,在Java6中substring方法会调用new string构造函数,此时会复用原来的char数组,而如果我们仅仅是用substring获取一小段字符,而原本string字符串非常大的情况下,substring的对象如果一直被引用,由于substring的里面的char数组仍然指向原字符串,此时string字符串也无法回收,从而导致内存泄露。 试想下,如果有大量这种通过substring获取超大字符串中一小段字符串的操作,会因为内存泄露而导致内存溢出。

    2019-05-25
    3
    137
  • 扫地僧
    答案是false,false,true。背后的原理是: 1、String str1 = "abc";通过字面量的方式创建,abc存储于字符串常量池中; 2、String str2 = new String("abc");通过new对象的方式创建字符串对象,引用地址存放在堆内存中,abc则存放在字符串常量池中;所以str1 == str2?显然是false 3、String str3 = str2.intern();由于str2调用了intern()方法,会返回常量池中的数据,地址直接指向常量池,所以str1 == str3;而str2和str3地址值不等所以也是false(str2指向堆空间,str3直接指向字符串常量池)。不知道这样理解有木有问题

    作者回复: 答案非常正确,理解了这个题目基本理解了string的特性了。

    2019-05-25
    12
    106
  • 快乐的五五开
    自学一年居然不知道有String.intern这个方法😓😓 不过从Java8开始(大概) String.split() 传入长度为1字符串的时候并不会使用正则,这种情况还是可以用

    作者回复: 非常感谢Geek的补充,我在这里也再补充一个小点,split有两种情况不会使用正则表达式: 第一种为传入的参数长度为1,且不包含“.$|()[{^?*+\\”regex元字符的情况下,不会使用正则表达式; 第二种为传入的参数长度为2,第一个字符是反斜杠,并且第二个字符不是ASCII数字或ASCII字母的情况下,不会使用正则表达式。

    2019-05-25
    3
    75
  • 风轻扬
    老师好,诚心请教一个问题 string s1 = new string(“1”)+new string(“1”); s1.intern; string s2=“11”; s1==s2为什么是true呢,我理解s1指向的对象,s2指向的常量池地址才对啊? 然后 string s1 = new string(“1”); s1.intern; string s2=“11”; s1==s2又是false了,区别在哪? 老师,周董提的这个问题,我都琢磨一晚上了。您的回答看了好多遍,确实是看不懂,您能再解释一下吗?目前的回答,咋看也看不懂。。。。。。

    作者回复: 如果看不太懂,建议先熟悉下JVM这块的知识点。我们知道,JVM从逻辑分区可以分为堆、JVM栈、本地方法栈、方法区、程序计数器,方法区中,在JDK1.8之后,包含了元空间、静态常量池、运行时常量池。 对于字符串常量,在类加载时,会将字符串放入方法区中的静态常量池,包括字符串的字面量和字符引用。而在初始化或运行时,会将字符引用转为直接引用,存放在运行时常量池。 如果是运行时动态生成的字符串对象调用intern方法,如果字符串的引用在运行时常量池不存在,则会在常量池中创建一个引用。 所以第一个通过加动态生成的“11”字符串由于在运行时常量中没有该字符串的引用,所以会在调用s1.intern时,在运行时常量池中生成一个s1的引用,当s2再次引用该字符串时,发现运行时常量池中存在相同值的字符串的引用,就直接返回s1的引用。所以s1==s2是返回的true。这也仅限于JDK1.7之后的版本。 而第二种,用于"11"在类加载时,已经存在静态常量池中,在new string(“11”)时,会在运行时常量池中创建一个“11”字符串的直接引用。而s1指向的并不是该引用,而是new string这个对象的引用。当s2=“11”时,返回的是运行时常量池中的引用。所以s1==s2返回false。

    2019-08-01
    16
    42
  • 周董
    老师,还有一个问题网上众说纷纭,jdk1.8版本,字符串常量池和运行时常量池分别在内存哪个区?您文中的常量池是什么常量池?调用intern后字符串是在哪个常量池生成引用或者对象?麻烦老师抽空解答下,这个困扰很久了。

    作者回复: 严格来说,是静态常量池和运行时常量池,静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。 运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。 这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。 我文中说的是两个常量池,没有具体区分,在初次加载时,是字面量是加载到了静态常量池中,解析之后会将引用加载到运行时常量池。 intern方法生成的引用或对象是在运行时常量池中。

    2019-08-01
    2
    31
  • Teanmy
    老师好,有一点始终想不明白,请老师解惑,非常感谢! 老师先帮忙看看关于这两行代码,我的分析是否正确: str1 = "abc"; str2 = new String("abc") str1 = "abc"; 1.str1,首先是在字符串常量池中寻找"abc",找到则取其地址,找不到则创建并返回其地址 2.将该地址赋值给栈中的str1 str2 = new String("abc") 1.在堆中创建String对象,我查阅了String构造方法源码,实际值取的是"abc"的(此时"abc"已经存在字符串常量池中)引用,也就是说,str2还是指向常量池,并没有创建新的"abc"。 public String(String original) { this.value = original.value; this.hash = original.hash; } 2.堆中创建完String对象,将该对象的地址赋值给栈变量str2 疑问: 既然不管是以上哪种方式,最终实际引用的还是常量池中的"abc",str2 = new String("abc")只是增加了一个堆中String的“空壳”对象而已(因为实际上char[]指向的还是常量池中的"abc"),这个空壳对象并不会占用过多内存。而.intern的实质只是减少了这个中间的String空壳对象,那何来twitter通过.intern减少大量内存?

    作者回复: 你好 teanmy。运行时创建的字符串对象只会在堆中创建一个对象。在这个前提下,如果有相同值的对象创建,使用intern可以减少重复字符串的创建。例如,有广东省/深圳市/南山区,如果有千万个人发布消息,创建了地址对象,这样导致千万个“广东省”对象在堆内存中创建,如果长时间引用,这些对象都没法释放,使用intern将“广东省”放到常量池中,其他对象引用常量池中的同一个“广东省”字符串,而堆中的千万个对象将被回收。 如果有疑问,请继续留言。

    2019-06-02
    4
    23
  • 失火的夏天
    开头题目答案是false false true str1是建立在常量池中的“abc”,str2是new出来,在堆内存里的,所以str1!=str2, str3是通过str2..intern()出来的,str1在常量池中已经建立了"abc",这个时候str3是从常量池里取出来的,和str1指向的是同一个对象,自然也就有了st1==str3,str3!=str2了

    作者回复: 这里我纠正下,str3是intern返回的引用,intern而不是创建出来的。 你的答案是正确的!

    2019-05-25
    21
  • Only now
    看了本篇几乎全部留言, 感觉包括老师在内, 对于 "字符串常量池" 和 "常量池", 这俩概念用的很混。 对于jdk7 以及之前的jvm版本不再去深究了, 它的字符串常量池存在于方法区, 但是jdk8以后, 它存在于Java堆中, 唯一, 且由java.lang.String类维护, 它和类文件常量池, 运行时常量池没有半毛钱的关系。 最后我有个疑问问老师, 字符串常量池中的对象, 在失去了所有外部引用之后, 会被gc掉吗?

    作者回复: 非常感谢only now的总结,这一讲中没有详细去区分常量池,而是在强调字符串的使用,后面我们在JVM中可以再一起研究下常量池。 JVM文献中提到方法区是存在垃圾回收。我们可以通过intern方法来验证这个gc问题,通过大量请求请求某个接口,传入参数创建字符串对象,之后通过intern方法在常量池中生成字符串对象,之后失去引用,观察gc情况。

    2019-05-29
    19
  • Zend
    “在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。” 比如: 是从常量池中复制到堆内存,这时常量池中字符串与堆内存字符串是完全独立的,内部也不存在引用关系?

    作者回复: 你好 Zend,具体的复制过程是先将常量池中的字符串压入栈中,在使用string的构造方法时,会拿到栈中的字符串作为构造方法的参数。这里我纠正一点,今天我查看了下这个构造函数,String的构造函数是一个char数组赋值过程,不是new char[]重新创建,所以是引用了常量池中的字符串对象,存在引用关系。

    2019-05-26
    14
  • 风翱
    使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。 像国家地区是有边界的。像其他情况,怎么把握这个度呢?

    作者回复: 如果对空间要求高于时间要求,且存在大量重复字符串时,可以考虑使用常量池存储。 如果对查询速度要求很高,且存储字符串数量很大,重复率很低的情况下,不建议存储在常量池中。 具体可以通过模拟测试自己的场景,对比两种存储方式的性能,通过数据来给自己答案。

    2019-05-25
    3
    13
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部