一次关于append方法和“+”对内存资源的影响的总结

昨天晚上测试了以下两组代码,其中一组很快就抛出了OOM(java.lang.OutOfMemoryError)错误,说的是java堆空间不足了,你们猜猜是哪一组代码:

        //代码一
        StringBuilder str = new StringBuilder();
        while (true) {
            str.append("1");
        }
        //代码二
        String str = "";
        while (true) {
            str = str + "1";
        }

经过测试,发现代码一会抛出OOM错误
那么这就让我很困惑了,不是说在大量拼接字符串时建议用append方法吗,那为什么现在又报错了呢,下面我就好好分析一下这个问题:

一.为什么建议在大量拼接字符串时采用append方法:

因为String底层是采用一个不可更改指向的字符数组来保存字符串中字符的:

private final char value[];    // final修饰不可更改value的指向

正是由于这个不可变更的value[],所以我们每次在做字符串拼接的时候,无法更换字符串底层的value[],只能重新创建出一个字符串对象。那么当我们用“+”进行字符串拼接时,jvm到底是如果新创建一个字符串对象的呢?为此,我们把代码二反编译一下看看:

 0: ldc           #2                  // String
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: ldc           #6                  // String 1
        16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_1
        23: goto          3

从反编译的代码中可以看出,jvm先构造出一个StringBuilder对象,再调用它的append方法先后拼接上原来的字符串和待拼接的字符串,也就是str"1",最后再调用StringBuilder的toString方法转化为String类型输出。

那么在循环中反复的拼接字符串就会导致大量的中间字符串产生,因为我们每拼接一次就会调用一次toString方法,产生一个中间字符串。这些中间字符串最后都是需要GC来回收的,这样就大大增加了GC的负担,也无意义的消耗了系统的资源。下面是代码二运行时堆内存的使用情况:
在这里插入图片描述
可以看出来,此时gc正在很努力的回收这些中间字符串,这会消耗大量的系统资源。而使用StringBuilder类中的value[]不是final修饰的,所以当value[]容量不够用时不必新创建StringBuilder对象,可以只改变value[]的指向即可(指向一个扩容后的char[]),这样就可以避免产生大量中间字符串消耗系统资源了。

2.为什么代码一会抛出OOM错误?

我们回到开头的问题,仔细理一理这两个代码的执行过程,看看有什么不同:

我们先看看StringBuilder中append方法的源码

    @Override
    public StringBuilder append(String str) {
        super.append(str);   //调用父类append方法 
        return this;
    }

发现他调用了父类的方法,再进入:

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);   //对value[]数组做扩容
        str.getChars(0, len, value, count);    //字符串迁移
        count += len;
        return this;
    }

这里主要看一下ensureCapacityInternal方法:

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));  //对原value[]做扩容
        }
    }

再进入newCapacity方法:

    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;  //value[]数组每次扩容都会翻倍再加2
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

因为每次StringBuilder底层字符串扩容时,value[]的容量会翻倍再+2,所以在循环中反复翻倍多次,最后形成的数组长度将大大超出我们的使用范围,其所占用的内存空间也是很大的。所以很快的抛出了OOM错误。

那么String类型字符串的拼接底层也是append方法啊,它为什么不会抛出OOM错误呢?其原因就是对普通字符串的拼接时,调用append方法后都会再调用一次toString方法,转化为普通字符串再赋值。

    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {     // 1
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

这样一来,每次jvm都会创建一个新的StringBuilder进行拼接,这就没法叠加翻倍value[]的长度了。并且当字符串长度大于2时,代码每次都会走进1处的if中,最终将最小所需长度赋值给value[],这样一来,value[]就没法多占用内存空间了。

所以当我们反复对一个字符串拼接时,最后StringBuilder会占用更大的内存,不过如果使用String进行拼接的时候,所带来的反复创建字符串对象的问题和给gc带来的压力也不容忽视。所以还是用StringBuilder进行字符串拼接把。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值