Java性能调优实战
刘超
金山软件西山居技术经理
立即订阅
7535 人已学习
课程目录
已完结 48 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 怎样才能做好性能调优?
免费
模块一 · 概述 (2讲)
01 | 如何制定性能调优标准?
02 | 如何制定性能调优策略?
模块二 · Java编程性能调优 (10讲)
03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据
04 | 慎重使用正则表达式
05 | ArrayList还是LinkedList?使用不当性能差千倍
加餐 | 推荐几款常用的性能测试工具
06 | Stream如何提高遍历集合效率?
07 | 深入浅出HashMap的设计与优化
08 | 网络通信优化之I/O模型:如何解决高并发下I/O瓶颈?
09 | 网络通信优化之序列化:避免使用Java序列化
10 | 网络通信优化之通信协议:如何优化RPC网络通信?
11 | 答疑课堂:深入了解NIO的优化实现原理
模块三 · 多线程性能调优 (10讲)
12 | 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法
13 | 多线程之锁优化(中):深入了解Lock同步锁的优化方法
14 | 多线程之锁优化(下):使用乐观锁优化并行操作
15 | 多线程调优(上):哪些操作导致了上下文切换?
16 | 多线程调优(下):如何优化多线程上下文切换?
17 | 并发容器的使用:识别不同场景下最优容器
18 | 如何设置线程池大小?
19 | 如何用协程来优化多线程业务?
20 | 答疑课堂:模块三热点问题解答
加餐 | 什么是数据的强、弱一致性?
模块四 · JVM性能监测及调优 (6讲)
21 | 磨刀不误砍柴工:欲知JVM调优先了解JVM内存模型
22 | 深入JVM即时编译器JIT,优化Java编译
23 | 如何优化垃圾回收机制?
24 | 如何优化JVM内存分配?
25 | 内存持续上升,我该如何排查问题?
26 | 答疑课堂:模块四热点问题解答
模块五 · 设计模式调优 (6讲)
27 | 单例模式:如何创建单一对象优化系统性能?
28 | 原型模式与享元模式:提升系统性能的利器
29 | 如何使用设计模式优化并发编程?
30 | 生产者消费者模式:电商库存设计优化
31 | 装饰器模式:如何优化电商系统中复杂的商品价格策略?
32 | 答疑课堂:模块五思考题集锦
模块六 · 数据库性能调优 (8讲)
33 | MySQL调优之SQL语句:如何写出高性能SQL语句?
34 | MySQL调优之事务:高并发场景下的数据库事务调优
35 | MySQL调优之索引:索引的失效与优化
36 | 记一次线上SQL死锁事故:如何避免死锁?
37 | 什么时候需要分表分库?
38 | 电商系统表设计优化案例分析
39 | 数据库参数设置优化,失之毫厘差之千里
40 | 答疑课堂:MySQL中InnoDB的知识点串讲
模块七 · 实战演练场 (4讲)
41 | 如何设计更优的分布式锁?
42 | 电商系统的分布式事务调优
43 | 如何使用缓存优化系统性能?
44 | 记一次双十一抢购性能瓶颈调优
结束语 (1讲)
结束语 | 栉风沐雨,砥砺前行!
Java性能调优实战
登录|注册

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

刘超 2019-05-25
你好,我是刘超。
从第二个模块开始,我将带你学习 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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java性能调优实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(91)

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

    共享char数组可能导致内存泄露问题?

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

    试想下,如果有大量这种通过substring获取超大字符串中一小段字符串的操作,会因为内存泄露而导致内存溢出。

    2019-05-25
    47
  • 扫地僧
    答案是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
    2
    31
  • 快乐的五五开
    自学一年居然不知道有String.intern这个方法😓😓
    不过从Java8开始(大概) String.split() 传入长度为1字符串的时候并不会使用正则,这种情况还是可以用

    作者回复: 非常感谢Geek的补充,我在这里也再补充一个小点,split有两种情况不会使用正则表达式:

    第一种为传入的参数长度为1,且不包含“.$|()[{^?*+\\”regex元字符的情况下,不会使用正则表达式;

    第二种为传入的参数长度为2,第一个字符是反斜杠,并且第二个字符不是ASCII数字或ASCII字母的情况下,不会使用正则表达式。

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

    作者回复: 如果对空间要求高于时间要求,且存在大量重复字符串时,可以考虑使用常量池存储。

    如果对查询速度要求很高,且存储字符串数量很大,重复率很低的情况下,不建议存储在常量池中。

    具体可以通过模拟测试自己的场景,对比两种存储方式的性能,通过数据来给自己答案。

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

    作者回复: 严格来说,是静态常量池和运行时常量池,静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。

    运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。

    这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。

    我文中说的是两个常量池,没有具体区分,在初次加载时,是字面量是加载到了静态常量池中,解析之后会将引用加载到运行时常量池。

    intern方法生成的引用或对象是在运行时常量池中。

    2019-08-01
    7
  • Eric
    对于您文中 “在一开始创建 a 变量时,会在堆内存中创建一个对象,同时在常量池中创建一个字符串对象” 这句话 我认为前部分没有问题 分歧点在后面那部分 我觉得abc常量早就在运行时常量池就存在了 可以理解使用这个类之前 就已经构造好了运行时常量池 而运行时常量池中就包括“abc”常量 至于使用new String(“abc”) 我觉得它应该只会在堆中创建String对象 并将运行时常量池中已经存在的“abc”常量的引用作为构造函数的参数而已

    作者回复: 你理解的分歧点是对的,这个构造是在加载类时,就已经在常量池中构造好常量。

    2019-05-25
    1
    7
  • 风轻扬
    老师好,诚心请教一个问题
    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
    6
  • 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
    6
  • Zend
    “在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。”
    比如:

    是从常量池中复制到堆内存,这时常量池中字符串与堆内存字符串是完全独立的,内部也不存在引用关系?

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

    2019-05-26
    6
  • Eric
    String s1 = new String("abc").intern()

    Code:
           0: new #2 // class java/lang/String
           3: dup
           4: ldc #3 // String abc
           6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
           9: invokevirtual #5 // Method java/lang/String.intern:()Ljava/lang/String;
          12: astore_1
          13: return

    9:invokevirtual的时候 常量池里面应该早就有了”abc“这个字符串常量了吧 为什么文中说的是先去堆中创建一个String对象 然后再去常量池创建一个字符串常量? 我理解错误了吗?

    作者回复: 我们可以看到0 new,即是生成了一个对象,这个对象是在堆内存用创建的,之后4 Idc则是将常量池中创建的字符串abc压入栈中,invokespecial调用构造方法复制abc字符串到对象中,invokevirtual调用intern本地方法,返回常量池中的对象引用给s1。

    new String("abc")是会创建两个对象的,一个是堆对象,一个是常量池中的对象,intern会去判断常量池中是否有,这个时候是有的,所以不会创建,而是改变s1的引用。

    不知道这样是否更好理解?

    2019-05-25
    6
  • Only now
    看了本篇几乎全部留言, 感觉包括老师在内, 对于 "字符串常量池" 和 "常量池", 这俩概念用的很混。

    对于jdk7 以及之前的jvm版本不再去深究了, 它的字符串常量池存在于方法区, 但是jdk8以后, 它存在于Java堆中, 唯一, 且由java.lang.String类维护, 它和类文件常量池, 运行时常量池没有半毛钱的关系。

    最后我有个疑问问老师, 字符串常量池中的对象, 在失去了所有外部引用之后, 会被gc掉吗?

    作者回复: 非常感谢only now的总结,这一讲中没有详细去区分常量池,而是在强调字符串的使用,后面我们在JVM中可以再一起研究下常量池。

    JVM文献中提到方法区是存在垃圾回收。我们可以通过intern方法来验证这个gc问题,通过大量请求请求某个接口,传入参数创建字符串对象,之后通过intern方法在常量池中生成字符串对象,之后失去引用,观察gc情况。

    2019-05-29
    5
  • 周董
    老师好,诚心请教一个问题
    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了,区别在哪?

    作者回复: String s1 = new String("1") + new String("1")会在堆中组合一个新的字符串对象"11",在s1.intern()之后,由于常量池中没有该字符串的引用(只有字符串常量"11"),所以常量池中生成一个堆中字符串"11"的引用,此时String s2= "11"返回的是堆字符串"11"的引用,所以s1==s2。

    在JDK1.7版本以及之后的版本运行以下代码,你会发现结果为true,在JDK1.6版本运行的结果却为false:
    String s1 = new String("1") + new String("1");
    System.out.println( s1.intern()==s1);



    而String s1 = new String("11")首先会在常量池中创建字符串"11"的引用,而s1则是返回的堆中的new String("11")对象的引用,此时s1.intern()返回的是常量池字符串常量"11"的引用,而非堆中的。而String s2="11"又是返回的常量池中常量"11"的引用。所以s1==s2为false。

    总结:常量池中同时存在字符串常量和字符串引用,在JDK1.7版本之后的intern()方法只会尝试对象的引用放入常量池,而在之前的版本中,intern()方法会复制字符串常量到常量池中,并返回字符串引用。

    2019-07-26
    3
    4
  • Hammy
    老师您好,我这里有一个疑问。在听您说完,对象的string属性实质上在运行中是在堆内存中创建而不是引用常量池的时候如雷贯耳一般,觉得自己之前根本没思考过这个问题,完全没想过用intern进行优化。但是我做了一个实验,public class Person {

        public String name;



        public void setName(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }


        public static void main(String[] args) {
            Person person1 = new Person();
            person1.setName("张三");
            Person person2 = new Person();
            person2.setName("张三");
            System.out.println(person1.name==person2.name);

        }
    这段代码中,我理解如果string是在运行过程中在堆内存生成对象,那么结果应该是false,但是返回的结果是true。这是我的一个疑惑,劳烦老师帮忙看一下我的测试代码哪里不对,还是有理解错误的地方。
    2019-06-21
    2
    3
  • -W.LI-
    老师好!第一个问题没有描述清楚。String
    a = ”abc”, String b =new String("abc"),String c=new String(new char[]{‘a’,‘b’,‘c’})。创建的String对象。我debug时发现这三个String对象的value指向的那个char数组地址值都是一样的。他们是复用了一个char数组么?还是工具显示问题?我用的idea。

    作者回复: 你好 W.LI,刚我debug了下,a和b的value是同一个地址,因为a在常量池中创建了"abc",而new String("abc")时,发现常量池存在"abc"字符串对象,不会创建了。这时通过构造函数String(String original)将常量池中的"abc"复制给value,这里的复制是引用,不是创建新的char[]数组,所以是同一个value地址。

    而c中的构造函数,是新开辟了一个char[]数组:
     public String(char value[]) {
            this.value = Arrays.copyOf(value, value.length);
     }

    所以value的地址不一样。

    可以再试试,有问题留言。

    2019-05-26
    3
  • Eric
    我在《Java虚拟机规范》里面看到一句话 这句话是当类或接口创建时,它的二进制表示中的常量池表被用来构造运行时常量池 我理解的意思是 类或接口 创建时就根据.class文件的常量池表生成了运行时常量池 执行new String("abc")这行代码应该只会生成一个String对象 并且调用它的构造函数 参数是运行时常量池里面"abc"字符串常量的Reference类型的数据(可以理解为指针吧)怎么会在这行代码执行的时候才会在运行时常量池生成”abc“对象呢?

    作者回复: 如果是需要按照创建顺序来讲,常量“abc”,则会在加载编译时构造常量池时在常量池中创建“abc”字符串对象,而new对象的构造函数是在运行时创建并复制常量池中的“abc”。还有一个运行时常量池,也就是说,在运行时创建的字符串对象,通过intern方法会在运行时常量池中创建字符串对象。

    2019-05-25
    1
    3
  • 建国
    在实际编码中我们应该使用什么方式创建字符传呢?
    A.String str= "abcdef";
    B.String str= new String("abcdef");
    C.String str= new String("abcdef"). intern();
    D.String str1=str.intern();

    作者回复: 实际编码中,我们要结合实际场景来选择创建字符串的方式,例如,在创建局部变量以及常量时,我们一般使用A的这种方式;如果我们要区别一个字符串创建两个不同的对象来使用时,会选择B;intern一般使用的比较少,例如我们平时会创建很多一样的字符串的对象时,且对象会保存在内存中,我们可以考虑使用intern方法来减少过多重复对象占用内存空间。

    2019-05-25
    1
    3
  • benben
    请教最后一张图第三列的意思是对象成员变量是string的话不会放到常量池是吗?

    作者回复: 是的,运行时动态创建是在堆内存中直接创建的,调用intern方法,会反倒常量池中。

    2019-06-26
    2
  • ° BugMaker
    刘老师您好!"使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担",那这个Twitter 工程师在 QCon 全球软件开发大会上的演讲的那个 intern 方法是如何做到遍历这么多常量池的数据,同时保证性能的呢?

    作者回复: 你好,如果我们的数据对查询速度没有这么高要求,可以考虑使用。

    2019-05-31
    2
  • 晓杰
    回答开篇的问题:
    str1会在常量池中创建一个对象
    str2首先会在堆内存中创建一个对象,然后在加载类的时候在常量池创建一个字符串对象,同时复制到堆内存对象中,并返回堆内存对象的引用
    str3会先去常量池中查看存在于该字符串相等的对象,因为str1已经在常量池创建了一个相同的对象,所以str1和str3相等。
    综上:str1和str2不相等,str1和str3相等,str2和str3不相等
    2019-05-26
    2
收起评论
91
返回
顶部