极客时间-java性能调优实战学习笔记

824 阅读6分钟

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

在文中作者抛出了这样一个问题

        String str1 = "abc";
        
        String str2  = new String("abc");

        String str3 = str2.intern();
        
        String str4 = "ab";

        String str5 =  str4+"c";

        System.out.println("str1==str2:"+str1 == str2);   //1

        System.out.println(str1.equals(str2));  //2

        System.out.println("str2==str3:"+str2 == str3);   //3

        System.out.println("str1==str3:"+str1==str3);     //4

        System.out.println("abc" == str1.substring(0));  //5

        System.out.println("bc" == str1.substring(1));  //6
        
        System.out.println(str4 == str5);  //7 
        
        

运行结果:

false
true
false
true
true
false

第一种(String str1 = "abc";)创建String字符串对象时,JVM首先会检查对象是否在字符串常量池中,如果在,就返回该对象的引用,否则新的字符串在常量池中被创建,这种方式可以减少同一个值的字符串对象的重复创建,节约内存.

第二种(String str2 = new String("abc");) 首先在编译类文件时,“abc”常量字符串将会放入到常量池结构中,在类加载时,“abc”将会在常量池中被创建;其次,在调用new时,JVM命令将会调用String 的构造函数,同时引用常量池中的“abc”字符串,在堆中创建一个String对象;最后,str将引用String对象

在Java中比较两个对象是否相等,往往用== ,判断两个对象的值是否相等,用equals()方法来判断

分析运行结果:

  • 1 、第一种方法仅仅是一个赋值语句,在创建的时候,JVM 会检查在字符串池中,是否已经存在该字符串,如果已经存在了,那么会返回这个字符串的引用给变量 s。如果不存在,那么会创建一个 abc 字符串对象,再赋值给 str1。因此,这句话可能只创建 1 个或者 0 个对象。 第二种方法会在内存中创建 1 个或者 2 个对象。把 new String(“abc”) 这句话拆成两个部分来看,一个是”abc”, 另一个是 new String()。如果 abc 字符串已经在字符串池中存在了,那么就不需要在创建 abc 字符串的对象了,但是 new String 这行代码会再构造出一个和 abc 一样的字符串,并且是放在堆上。 因此str1==str2 为false

  • 2、String重写了equals方法,str1和str2的值相等,因此str1.equals(str2)为true

  • 3、str2是使用堆中对象,str3由于是调用的intern()方法,会将str3的引用指向字符串常量池,所用引用的是字符串常量池中的对象.

  • str3intern()返回的引用指向常量池中的"abc",str1已经在常量池中建立了"abc",这个时候str3是从常量池中取出来的和str1指向的是同一个对象,所以为true

  • 5和6 sbuString()方法的实现中有个index == 0 的判断

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

当inde等于0时直接返回当前对象,否则新创建一个对象返回,而==又是地址比较,所以5为true,6为false

  • 7、当字符串常量与String类型变量连接时得到的新的字符串不再保存在常量池中,而是在堆中新建一个String对象存放,很明显常量池中要求的存放的是常量,有 String 类型变量当然不能存在常量池中了。

如何使用String.intern节省内存

String a =new String("abc").intern();
String b = new String("abc").intern();
    	  
if(a==b) {
    System.out.print("a==b");
}

输出结果:

a==b

在字符串常量中,默认会将对象放入常量池中;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,引用赋值到堆内存对象中,并返回堆内存对象引用.

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

了解了上面的原理,我们可以得知:

在一开始创建a变量时,会在堆内存中创建一个对象,同时会在加载类时,在常量池中创建一个字符串对象,调用intern方法之后,会去常量池中查找是否有等于该字符串的对象,有就返回引用.

在创建b字符串变量时,也会在堆中创建一个对象,此时常量池中有该字符串对象,就不再创建.调用intern方法则会去常量池中判断是否有等于该字符串的对象.发现有等于"abc"字符串的对象,就直接返回引用.而在堆内存中的对象,由于没有引用指向他,将会被垃圾回收器回收掉.所以a和b引用的是同一个对象.

下图为String字符串的创建分配内存地址的情况:

常量池是类似于一个HashTable的实现方式,HastTable存储的数据越大, 遍历的时间复杂度就会增加,如果数据过大,会增加整个字符串常量池的负担.

如何使用字符串的分割

spit()方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是十分不稳定的,使用不恰当会引起回溯问题,导致CPU居高不下.

可以使用String.indexOf()来替代.

05|ArrayList还是LinkedList?使用不当性能差千倍

ArrayList属性

  // 默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;
    // 对象数组
    transient Object[] elementData; 
    // 数组长度
    private int size;

ArrayList的属性elementData被关键字transient修饰,transient关键字修饰该字段则表示该属性不会吧被序列化,但ArrayList其实是实现来序列化接口.由于ArrayList的数组是基于动态扩增的,所以并不是所有被分配的内存空间都存储了数据.如果采用外部序列化的方式实现数组的序列化,会序列化整个数组.ArrayList为避免这些没有存储数据的空间被序列化,内部提供了两个私有化的方法writeObect以及readObject来自我完成序列化与发序列化,从而在序列化与发序列化数组时节省来空间和时间.

因此使用transient修饰数组,是防止对象数组被其他外部方法序列化.

ArrayList新增元素

如果我们在初始化时就比较清楚存储数据的大小,就可以在ArrayList初始化时指定数组容量的大小,并且在添加元素时,只在数组末尾添加元素,那么ArrayList在大量新增元素的情况下,性能并不会变差,反而比其他List集合的性能更好

ArrayList删除元素

ArrayList没删除一个元素,都要进行数组重组,并且删除的元素越靠前,数组重组的开销越大.

LinkedList是如何实现的?

在jdk 1.7以后,LinkedList做来很大的改动,链表的Entry结构变成了Node ,内部组成基本没有改变,但LinkedList里面的header属性去掉了,新增了一个Node结构的first属性和一个Node结构的last属性,这样做有以下几个好处: