终于搞懂了!字符串拼接的各种姿势以及底层的小知识

前言

最近在路上突然在想Java String和String Buffer和String Builder在日常工作中的使用,这就不得不提到之前写的各种String + ""的操作,又想起JVM的各种优化,就不禁想知道这个优化具体是什么样的。

一、 字符串拼接的小知识

在查找相关资料的过程中,了解到类似于如下代码

    public String concatString(){
        String a = "a";
        String b = "b";
        String c = "c";
        return a + b + c;
    }

JVM在编译的时候会进行优化,将他们转化为StringBuilder.append的形式(JDK1.5之后,1.5之前使用的是StringBuffer.append
这么说除了网络上的说法,我们也要亲眼看看,首先 生成字节码(.class)

javac xxx.jar

反编译字节码文件,查看源码

javap -c xxx.class

在这里插入图片描述
小结:
也就是说哪怕这种不规范的写法,Java底层页帮我们搞定了,这里需要注意两个点

  1. 哪怕底层有优化我们也要避免这种写法,根据情况选择使用StringBuilder/String Builder,如果需要循环,将StringBuilder/String Builder对象放在循环外,如果放在循环内部,会造成多次生成对象的浪费(不使用StringBuilder/String Builder,循环100次字符串和变量拼接实际上就是创建了100个StringBuilder/String Builder)。
  2. 注意,底层优化字符串拼接为String Builder的前提是变量和字符串的拼接,字符串常量的拼接会直接优化为一个字符串。
  3. JDK1.5 之前,使用的是StringBuffer,由于它是线程安全的,StringBuffer每次append都会进行锁的申请,浪费了不必要的时间,这就引出了锁消除。

二、锁消除和锁粗化

2.1 锁消除

锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。

就比如在一个完全没有多线程和方法逃逸(等下会讲),完全作为局部变量使用的情况,使用了带有线程安全的StringBuffer的append方法(从源码中可以看出,append方法用了synchronized关键词)。
就比如最简单的这段代码

    public String concatString(){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("a");
        return stringBuffer.toString();
    }

这个方法的[stringBuffer]对象,只在方法内部有效,多线程同时访问也是生成各自不同的stringBuffer对象,这时候进行线程同步只会造成资源的浪费。
以下引用自参考资料1

这时我们可以通过编译器将其优化,将锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

逃逸分析:比如上面的代码,它要看stringBuffer这个对象是否可能逃出它的作用范围[方法内]?如果将stringBuffer作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题这时就可以说stringBuffer这个对象发生逃逸了,因而不应将append操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。

其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。

2.2 锁粗化

锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们一种思想,有些情况(同一段代码,申请多次同一个锁,中间的代码很快就能完成)下可以很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

三、逃逸分析

3.1 逃逸分析是什么

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。
如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中被访问到的地方无法确定——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,因为全局变量是可以在当前子程序之外访问的,此时指针也发生了逃逸。

从代码层面来说,一共有3种逃逸场景

class A {
    public static B b;

    public void globalVariablePointerEscape() { // 给全局变量赋值,发生逃逸  
        b = new B();
    }

    public B methodPointerEscape() { // 方法返回值,发生逃逸  
        return new B();
    }

    public void instancePassPointerEscape() {
        methodPointerEscape().printClassName(this); // 实例引用传递,发生逃逸  
    }
}

class B {
    public void printClassName(A a) {
        System.out.println(a.class.getName());
    }
}

分别是 全局变量赋值,方法返回值,实例引用传递。

简单点来说,就是局部变量/对象因为各种情况跑到了他的作用范围之外(方法、线程),让另一个方法/线程访问到了原本不能被访问的变量和对象。
全局变量就是另一种情况了,类似线程安全,多线程访问同一个变量可能造成的各种线程安全问题,

从逃逸分析的作用范围来看,分为

  1. 方法逃逸
  2. 线程逃逸
3.2 逃逸分析对性能的影响

逃逸分析,是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

当对象出现逃逸,方法执行完成后,本该被GC回收的变量/对象,因为逃逸无法进行回收,会造成对性能的影响。

通过类似于参考资料5,我们可以知道,开启逃逸分析,JIT编译时能提高不少的性能。由此可知,方法/线程逃逸对性能造成的影响还是挺大的。

3.3 逃逸分析的优化

在java应用里普遍存在一种场景。一般是在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸(在方法体内,未将引用暴露给外面)。

按照JVM内存分配机制,首先会在堆里创建变量类的实例,然后将返回的对象指针压入调用栈,继续执行。

这是优化前,JVM的处理方式。
逃逸分析的优化有以下几种:

  1. 栈上分配
    如果能够通过逃逸分析确定对象不会被外部访问到,可以将该对象在栈上分配内存。栈上分配就是把方法中的变量和对象分配到栈上(无需在堆中创建),方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能。

  2. 同步消除
    线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争。单线程中是没有锁竞争。(锁和锁块内的对象不会逃逸出线程就可以把这个同步块取消)

  3. 标量替换
    原始数据类型(int,long等数值类型以及reference类型等)称为标量,创建对象时通过创建某方法使用到的成员变量来代替。
    就是不在堆中创建对象,而是创建这个对象的成员变量来达到提高程序性能的目的。

关于逃逸分析涉及到JVM优化,我打算之后专门写几篇文章来讲述,可以先看看参考文章。

四、总结

了解了字符串各种方式的拼接原理,总结一下

  1. String对象和String对象或者字符串常量进行 ”+“ 拼接的时候,JDK1.5之后底层实际会优化成StringBuilder,1.5之前为StringBuffer。(多次循环会浪费资源)
  2. 字符串常量在进行 ”+“ 拼接时,JVM会直接优化成一个字符串,并不会像想象种那样浪费空间。
  3. (突然想起来)字符串常量在 ”+“ 上一个空字符串(”“)的时候,实际上内存地址还是指向原来的字符串常量。
  4. 锁消除是发生在编译器级别的一种锁优化方式。它会在我们代码完全不存在线程安全危险的情况下,消除锁,来节省频繁申请释放锁造成的资源浪费。
  5. 逃逸分析是一种分析算法,来识别因为逃逸造成的性能减弱,选择对应优化方式来进行优化。可以说它是优化算法的前提,就是逃逸分析优化必须先打开逃逸分析(具体过程这里就不说了,想知道的可以看看参考资料)。

参考资料

  1. Java锁消除和锁粗化
  2. 什么是逃逸分析(Escape Analysis)?
  3. Java-JVM-逃逸分析
  4. JVM之逃逸分析
  5. JVM逃逸分析对性能的影响
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值