标签: String

你可能也会掉进这个简单的 String 的坑

作者的同学是某大公司高级开发工程师,某日收到不少错误告警信息,于是便去开始排查。

跟踪日志发现是某个服务抛出的异常信息,奇怪的是这个服务上线也有一段时间了。之前很少看到类似的错误信息,*近偶尔多了起来。

后来才定位到是因为服务调用了某外部接口,发现对方对参数长度做了限制,如果输入参数超过 1000 bytes,就直接抛异常,代码类似如下:

  1. /**
  2.  * @param status
  3.  * @param result, the size should less than 1000 bytes
  4.  * @throws Exception
  5.  */
  6. public XXResult(boolean status, String result) {
  7.     if (result != null && result.getBytes().length > 1000) {
  8.         throw new RuntimeException(“result size more than 1000 bytes!”);
  9.     }
  10.   ……
  11. }

心想,这还不简单,咱们的 result 也不是什么关键性的东西,你有限制,我直接 trim 一下不就行了?

%title插图%num

%title插图%num解决方案

于是三下五除二,给搞了个 trim 方法,支持传不同参数按需 trim,代码如下:

  1. /**
  2.  * 将给定的字符串 trim 到指定大小
  3.  * @param input
  4.  * @param trimTo 需要 trim 的字节长度
  5.  * @return trim 后的 String
  6.  */
  7. public static String trimAsByte(String input, int trimTo) {
  8.     if (Objects.isNull(input)) {
  9.         return null;
  10.     }
  11.     byte[] bytes = input.getBytes();
  12.     if (bytes.length > trimTo) {
  13.         byte [] subArray = Arrays.copyOfRange(bytes, 0, trimTo);
  14.         return new String(subArray);
  15.     }
  16.     return input;
  17. }

再在需要调用外部服务的地方,先调用这个 trimAsByte 方法,一顿操作连忙上线,一切完美~

%title插图%num

%title插图%num灾难现场

一切完美,作者也是这样认为的。然后幸福总是短暂的。

经过一段时间后(前面也提到,业务场景确实是偶发的),相同的错误仍然发生了。

简直不敢相信,都 trim 了为啥还会超出?你也帮忙想想,是哪里的问题?

%title插图%num

%title插图%num

看看上面的例子(为了方便展示,简单修改文首代码了下),

trimAsByte("WeChat:tangleithu"8)

输入字符串 WeChat:tangleithu 太长了,只 trim 到剩下 8 个字节,对应的字节数组是从 [87,101,67,104,97,116,58,116,97,110,103,108,101,105,116,104,117] 变为了 [87,101,67,104,97,116,58,116],字符串变成了 WeChat:t ,结果正确。

其实在写这个方法的时候还是太草率了,本应该很容易想到中文的情况的,我们来试试:

  1. trimAsByte(“程序猿石头”8)

%title插图%num

看上述截图,悲剧了,输入程序猿石头,3 个字节一个汉字,一共 15 个字节 [-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76],trim 到 8 位,剩下前 8 位 [-25,-88,-117,-27,-70,-113,-25,-116] 也正确。再 new String,又变成3 个 “中文” 了,虽然第 3 个“中文”,咱也不认识,咱也不敢问到底读啥,总之再转换成字节数组,长度多了 1 个,变成 9 了。

%title插图%num

问题算是定位到了。

%title插图%num不禁要问,为什么?

来看看这个 String 的构造函数,看看上面注释才发现,其实我们忽略了一个很重要的概念,就是编码方式。

  1. /**
  2.  * Constructs a new {@code String} by decoding the specified array of bytes
  3.  * using the platform’s default charset.  The length of the new {@code
  4.  * String} is a function of the charset, and hence may not be equal to the
  5.  * length of the byte array.
  6.  *
  7.  * <p> The behavior of this constructor when the given bytes are not valid
  8.  * in the default charset is unspecified.  The {@link
  9.  * java.nio.charset.CharsetDecoder} class should be used when more control
  10.  * over the decoding process is required.
  11.  *
  12.  * @param  bytes
  13.  *         The bytes to be decoded into characters
  14.  *
  15.  * @since  JDK1.1
  16.  */
  17. public String(byte bytes[]) {
  18.     //this(bytes, 0, bytes.length);
  19.     checkBounds(bytes, offset, length);
  20.     this.value = StringCoding.decode(bytes, offset, length);
  21. }

当我们用默认的构造函数 new String 的时候,只是用了系统默认的编码(本文是“UTF-8”)去尝试解码,构造出字符串。

所以,当我们在用字节数组(字节流)来表达具体的语义的时候,一定要约定好以什么方式进行编码,本文不具体阐述编码问题了。下面用一个例子来解释上文的现象:

%title插图%num

[-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76] 仍然用这串字节数组来实验,这串字节数组,如果用 “UTF-8” 编码去解释,那么其想表达的语义就是中文“程序猿石头”,从上文标注的 1,2,3 中可以看出来,没有写即用了系统中的默认编码“UTF-8”。

假设按照 “GBK” 来解释(标注 4),就是表达的 “绋嬪簭鐚跨煶澶�”,注意看下其中的 � 是不是似曾相识;

注意标注 5,通过 GBK 解释构造字符串后,再通过默认的 “UTF-8” 获取字节数组,长度就变成 24 了,然后还通过 “GBK” 编码得到的字节数组长度为 15(标注 6),再试图构造字符串(标注 7),其中“程序猿石头”的“头”字,已经没了。说明这个转换过程中,其实信息已经被丢了。

上面的 � 其实是 UNICODE 编码方式中的一个特殊的字符,也就是 0xFFFD(65535),其实是一个占位符(REPLACEMENT CHARACTER),用来表达未知的、没办法表达的东东。上文中在进行编码转换过程中,出现了这个玩意,其实也就是没办法准确表达含义,会被替换成这个东西,因此信息也就丢失了。你可以试试前面的例子,比如把前 8 个字节中的*后一两个字节随便改改,都是一样的。

%title插图%num

程序猿石头:65533 示例

%title插图%num

总结

总结一下,其实本来是一个很简单的问题,却经过几次修改才*终解决,说明对 “基础” 掌握得还是不够,一个重要的点是,在处理二进制数据的时候,一定要联想到 “编码” 方式。

另外,提醒我们,看似简单的问题,我们往往容易忽略。比如如果单纯看到文中提到的这个trim 方法,其实很容易写个单元测试就能尽早发现有问题;

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 )保存到内部字符串常量列表中。如下图所示:

%title插图%num

如果虚拟机后面又发现了一个相同的字符串常量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>() {
*  @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 使用可能造成的性能问题以及解决方法,就总结到这。若有错漏,欢迎补充。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速