文档章节

Android 性能优化之String篇

fuchenxuan
 fuchenxuan
发布于 2016/11/05 10:00
字数 3190
阅读 695
收藏 22

Android 性能优化之 String篇

关于String相关知识都是老掉牙的东西了,但我们经常可能在不经意的String 字符串拼接的情况下浪费内存,影响性能,也常常会成为触发内存OOM的最后一步。
所以本文对String字符串进行深度解析,有助于我们日常开发中提高程序的性能,解决因String 而导致的性能问题。

首先我们先回顾一下String类型的本质

String类型的本质

先看一下String的头部源码

/** Strings are constant; their values cannot be changed after they * are created. String buffers support mutable strings. * Because String objects are immutable they can be shared. * @see StringBuffer * @see StringBuilder * @see Charset * @since 1.0 */
public final class String implements Serializable, Comparable<String>, CharSequence {

    private static final long serialVersionUID = -6849794470754667710L;

    private static final char REPLACEMENT_CHAR = (char) 0xfffd;

打开String的源码,类注释中有这么一段话“Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.”。

这句话总结归纳了String的一个最重要的特点:

String是值不可变(immutable)的常量,是线程安全的(can be shared)。
接下来,String类使用了final修饰符,表明了String类的第二个特点:String类是不可继承的。

String类表示字符串。java程序中的所有字符串,如“ABC”,是实现这个类的实例

字符串是常量,它们的值不能被创建后改变。支持可变字符串字符串缓冲区。因为字符串对象是不可改变的,所以它们可以被共享。例如:

String str = "abc";

相当于

String s = new String("abc");

这里实际上创建了两个String对象,一个是”abc”对象,存储在常量空间中,一个是使用new关键字为对象s申请的空间,存储引用地址。

在执行到双引号包含字符串的语句时,JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里,如上面所示,str 和 s 指向同一个引用.

String的定义方法归纳起来总共为以下四种方式:

  • 直接使用”“引号创建;
  • 使用new String()创建;
  • 使用new String(“abcd”)创建以及其他的一些重载构造函数创建;
  • 使用重载的字符串连接操作符+创建。

常量池

在讨论String的一些本质,先了解一下常量池的概念java中的常量池(constant pool)技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复重复创建相等变量时节省了很多时间。常量池其实也就是一个内存空间,不同于使用new关键字创建的对象所在的堆空间。
在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。常量池还具备动态性(java.lang.String.intern()),运行期间可以将新的常量放入池中。

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

java中基本类型的包装类的大部分都实现了常量池技术,
即Byte,Short,Integer,Long,Character,Boolean;

Java String对象和字符串常量的关系?

JAVA中所有的对象都存放在堆里面,包括String对象。字符串常量保存在JAVA的.class文件的常量池中,在编译期就确定好了。

比如我们通过以下代码块:

String s = new String( "myString" );

其中字符串常量是”myString”,在编译时被存储在常量池的某个位置。在运行阶段,虚拟机发现字符串常量”myString”,它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对( [myString], s1 )保存到内部字符串常量列表中。如下图所示:

image

如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。

例如,String s2 = “myString”,运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。但是String对象s在堆里的一个不同位置,所以和s1不相同。

JAVA中的字符串常量可以作为String对象使用,字符串常量的字符序列本身是存放在常量池中,在字符串内部列表中每个字符串常量的字符序列对应一个String对象,实际使用的就是这个对象。

这个目前网上阐述的最多关于这个String对象和字符串常量的关系,网上各有说法,但是这个猜想也是有问题的
引自感谢博主
http://blog.csdn.net/sureyonder/article/details/5569366

String 在 JVM 的存储结构

String 在 JVM 的存储结构
一般而言,Java 对象在虚拟机的结构如下:
对象头(object header):8 个字节
Java 原始类型数据:如 int, float, char 等类型的数据,各类型数据占内存如 表 1. Java 各数据类型所占内存.
引用(reference):4 个字节
填充符(padding)

如果对于 String(JDK 6)的成员变量声明如下:

private final char value[]; 
  private final int offset; 
  private final int count; 
  private int hash;

JDK6字符串内存占用的计算方式:

首先计算一个空的 char 数组所占空间,在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度,即 8 + 4 = 12 字节 , 经过填充后为 16 字节。

那么一个空 String 所占空间为:

对象头(8 字节)+ char 数组(16 字节)+ 3 个 int(3 × 4 = 12 字节)+1 个 char 数组的引用 (4 字节 ) = 40 字节。

因此一个实际的 String 所占空间的计算公式如下:

8*( ( 8+12+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )

其中,n 为字符串长度。

String 方法很多时候我们移动客户端常用于文本分析及大量字符串处理,
比如高频率的拼接字符串,Log日志输出,会对内存性能造成一些影响。可能导致内存占用太大甚至OOM。
频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。

String 一些提高性能方法

String的contact()方法

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    char buf[] = new char[count + otherLen];
    getChars(0, count, buf, 0);
    str.getChars(0, otherLen, buf, count);
    return new String(0, count + otherLen, buf);
    }

这是concat()的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的:return new String(0, count + otherLen, buf);这同样也创建了10W个字符串对象,这是它变慢的根本原因。

String的intern()方法

当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。

例如:

“abc”.intern()方法的返回值还是字符串”abc”,表面上看起来好像这个方法没什么用处。但实际上,它做了个小动作:
检查字符串池里是否存在”abc”这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把”abc”添加到字符串池中,然后再返回它的引用。

String s1 = new String("111");
String s2 = "sss111";
String s3 = "sss" + "111";
String s4 = "sss" + s1;
System.out.println(s2 == s3); //true
System.out.println(s2 == s4); //false
System.out.println(s2 == s4.intern()); //true

过多得使用 intern()将导致 PermGen 过度增长而最后返回 OutOfMemoryError,因为垃圾收集器不会对被缓存的 String 做垃圾回收,所以如果使用不当会造成内存泄露。

关于截取字符串方法的性能比较

  • 对于从大文本中截取少量字符串的应用,String.substring()将会导致内存的过度浪费。
  • 对于从一般文本中截取一定数量的字符串,截取的字符串长度总和与原始文本长度相差不大,现有的 String.substring()设计恰好可以共享原始文本从而达到节省内存的目的。

更多详细比较请查看这篇博文
http://blog.csdn.net/songylwq/article/details/9016609

使用StringBuilder 提高性能

在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 String 对象。但是如何正确的使用StringBuilder呢?

初始合适的长度

StringBuilder继承AbstractStringBuilder,打开AbstractStringBuilder的源码

/** * A modifiable {@link CharSequence sequence of characters} for use in creating * and modifying Strings. This class is intended as a base class for * {@link StringBuffer} and {@link StringBuilder}. * * @see StringBuffer * @see StringBuilder * @since 1.5 */
abstract class AbstractStringBuilder {

    static final int INITIAL_CAPACITY = 16;

    private char[] value;

    private int count;

    private boolean shared;

我们可以看到
StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。
new StringBuilder(),并且 时char[]的默认长度是16,

private void enlargeBuffer(int min) {
        int newCount = ((value.length >> 1) + value.length) + 2;
        char[] newData = new char[min > newCount ? min : newCount];
        System.arraycopy(value, 0, newData, 0, count);
        value = newData;
        shared = false;
    }

然后如果StringBuilder的剩余容量,无法添加全部内容,如果要append第17个字符,怎么办?可以看到enlargeBuffer函数,用System.arraycopy成倍复制扩容!导致内存的消耗,增加GC的压力。
这要是在高频率的回调或循环下,对内存和性能影响非常大,或者引发OOM。

同时StringBuilder的toString方法,也会造成char数组的浪费。

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

我们的优化方法是StringBuilder在append()的时候,不是直接往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

重用的StringBuilder

/** * 参考BigDecimal, 可重用的StringBuilder, 节约StringBuilder内部的char[] * * 参考下面的示例代码将其保存为ThreadLocal. * * <pre> * private static final ThreadLocal<StringBuilderHelper> threadLocalStringBuilderHolder = new ThreadLocal<StringBuilderHelper>() { * &#64;Override * protected StringBuilderHelper initialValue() { * return new StringBuilderHelper(256); * } * }; * * StringBuilder sb = threadLocalStringBuilderHolder.get().resetAndGetStringBuilder(); * * </pre> */
    public class StringBuilderHolder {

        private final StringBuilder sb;

        public StringBuilderHolder(int capacity) {
            sb = new StringBuilder(capacity);
        }

        /** * 重置StringBuilder内部的writerIndex, 而char[]保留不动. */
        public StringBuilder resetAndGetStringBuilder() {
            sb.setLength(0);
            return sb;
        }
    }

这个做法来源于JDK里的BigDecimal类

Log真正需要时候做拼接

对于那些需要高频率拼接打印Log的场景,封装一个LogUtil,来控制日志在真正需要输出时候才去做拼接。比如:

public void log(String  msg ){
        if (BuildConfig.DEBUG){
            Log.e("TAG","Explicit concurrent mark sweep " +
                    "GC freed 10477(686KB) AllocSpace objects, 0(0B) " +
                    "LOS objects, 39% free, 9MB/15MB, paused 915us total 28.320ms"+msg);
        }
    }

总结几个简单题目

String s1 = new String("s1") ; 
String s2 = new String("s1") ;

上面创建了几个String对象?

答案:3个 ,编译期Constant Pool中创建1个,运行期heap中创建2个.

String s1 = "s1";  
String s2 = s1;  
s2 = "s2";

s1指向的对象中的字符串是什么?

答案: “s1”

总结

关于String 性能优化,了解String 在 JVM 中的存储结构,String 的 API 使用可能造成的性能问题以及解决方法,就总结到这。若有错漏,欢迎补充。

参考文章
https://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/

更多Android 之美,请阅读《Android 之美 从0到1 – 高手之路》系列文章

水平有限,若有错漏,欢迎指正,批评,如需转载,请注明出处–http://blog.csdn.net/vfush,谢谢!

作者:fuchenxuan
出处:http://blog.csdn.net/vfush
欢迎访问我的个人站点:http://fuchenxuan.cn
转载请注明出处–http://blog.csdn.net/vfush

Android 之美 从0到1

© 著作权归作者所有

fuchenxuan
粉丝 10
博文 29
码字总数 52850
作品 0
南昌
私信 提问
加载中

评论(1)

OSC_vlkPzm
OSC_vlkPzm
不明觉厉
性能优化之Java(Android)代码优化

最新最准确内容建议直接访问原文:性能优化之Java(Android)代码优化 本文为Android性能优化的第三篇——Java(Android)代码优化。主要介绍Java代码中性能优化方式及网络优化,包括缓存、异步、...

Trinea
2013/08/26
2.5K
1
转载:最强最全干货分享:Android开发书籍、教程、工具等

最全干货分享,本文收集整理了Android开发所需的书籍、教程、工具、资讯和周刊各种资源,它们能让你在Android开发之旅的各个阶段都受益。 入门 《Learning Android(中文版)》 本书为Andro...

拉偶有所依
2015/01/09
152
3
最强最全干货分享:Android开发书籍、教程、工具等

最全干货分享,本文收集整理了Android开发所需的书籍、教程、工具、资讯和周刊各种资源,它们能让你在Android开发之旅的各个阶段都受益。 入门 《Learning Android(中文版)》 本书为Andro...

拉偶有所依
2015/01/09
0
1
Android性能优化之数据库优化

最新最准确内容建议直接访问原文:性能优化之数据库优化 本文为性能优化的第一篇——数据库性能优化,原理适用于大部分数据库包括Sqlite、Mysql、Oracle、Sql server,详细介绍了索引(优缺点...

Trinea
2013/08/21
4.8K
1
国内地区Docker上安装battery-historian

首先,我们需要将Battery Historian工具在本地跑起来,要跑起来可以选择使用docker,也可以选择编译源码。 (1)如果是使用Mac或Linux平台的话,推荐直接通过docker运行Battery Historian来完成...

SuShine
2018/08/01
0
0

没有更多内容

加载失败,请刷新页面

加载更多

ngrok 外网映射工具

ngrok介绍 许多刚学java web的同学(包括我自己)肯定都非常好奇,如何在外网上访问自己做的项目,跟我们本地访问tomcat有什么区别? 今天就向大家介绍一个非常强大的外网映射工具:ngrok.ngrok可以...

edison_kwok
55分钟前
2
0
Spark Streaming的优化之路——从Receiver到Direct模式

          作者:个推数据研发工程师 学长 1 业务背景 随着大数据的快速发展,业务场景越来越复杂,离线式的批处理框架MapReduce已经不能满足业务,大量的场景需要实时的数据处理结果来...

个推
今天
4
0
壮丽70年·奋斗新时代|蒸妙集团熏蒸中会阴熏蒸的神奇好处

聚结相合之处为会。会阴居两阴间,为督、任、冲三脉的起点,三脉背出两阴之间,会聚阴部,因名会阴。会阴,经穴名。出《针灸甲乙经》。会阴别名屏翳、下极、金门。属任脉。在会阴部,男性当阴...

公益传承
今天
2
0
pentaho-kettle-8.2.0.0-R源码开发环境搭建

1.从Kettle官网下载源码,本文使用的是pentaho-kettle-8.2.0.0-R 下载地址:https://codeload.github.com/pentaho/pentaho-kettle/zip/8.2.0.0-R 2.打开eclipse,选择一个新的工作空间,然后设...

gq_2010
今天
1
0
lua web快速开发指南(7) - 高效的接口调用 - httpc库

httpc库基于cf框架都内部实现的socket编写的http client库. httpc库内置SSL支持, 在不使用代理的情况下就可以请求第三方接口. httpc支持header、args、body、timeout请求设置, 完美支持各种h...

水果糖的小铺子
今天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部