从字节码视角看java字符串的拼接
来源:互联网 发布:运营优化具体体现在 编辑:程序博客网 时间:2024/05/17 18:27
搞java的都知道,string直接用+拼接的时候,javac编译会进行优化,因此字符串拼接也推荐使用stringbuffer或者stringbuilder。那到底是怎么优化的呢?简单的代码如下
package test;public class Java {public String test(String s1, String s2) {return s1 + s2;}public String test1(String s1, String s2) {return new StringBuilder(s1).append(s2).toString();}}
服务端
服务端我们知道jvm是基于栈实现的,但编译的时候具体怎么做优化了呢,javap工具闪亮登场
+拼接
public java.lang.String test(java.lang.String, java.lang.String); flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=3 0: new #16 // class java/lang/StringBuilder 3: dup 4: aload_1 5: invokestatic #18 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; 8: invokespecial #24 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 11: aload_2 12: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: invokevirtual #31 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 18: areturn LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 19 0 this Ltest/Java; 0 19 1 s1 Ljava/lang/String; 0 19 2 s2 Ljava/lang/String;
简单说明一下:
flags:标识方法的访问权限,此方法是public的code:包括完整的编译后指令、局部变量等信息stack:栈大小,服务端jvm是基于栈实现的,后面再分析为什么为3
locals:局部变量占用大小,后面分析为什么为3
LineNumberTable:对应源代码行,这个debug的时候很有用了
LocalVariableTable:局部变量表start,length:此两个字段,组合起来标识变量的使用范围,如0,19,则标识这个变量在从第一个指令开始到最后一直有效(因为s1,s2是入参,),可以尝试在代码中间定义个局部变量,就可以看到start和length的不同。slot:JVM规范里面对局部变量里存储一个局部变量的存储单元的叫法,是32位4个字节,可以尝试定义long,就可以发现会占用2个slot。这里三个都是ref引用,因此locals为3,即三个slot。中间一大块,明显可以看到,string用+拼接,已经被优化为使用stringbuilder了,具体的指令含义,可参考http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings比如
aload_1,就是把局部变量里面的s1,压入到栈当中。jvm提供了aload_0到aload_3共类似4个指令,那如果局部变量超过4个怎么办呢,当然还有aload指令,直接指定局部变量就好了。我们知道服务端的jvm是基于栈实现的,上面的aload就是把局部变量压入栈当中,因此在编译后已经计算好栈的长度,也就是
stack大小,上述例子中,stringBuilder,s1,s2三个局部变量都会压入栈,因此操作数栈3个就足够了,那如果是两个int相加(int a=1+2)返回呢?是否也是三个呢,答案是2,如果是两个long相加又是多少呢?(long要占用8个字节)
看两个字符串的直接+拼接方法,已经优化为采用stringbuilder进行了拼装。
stringbuilder拼接
public java.lang.String test1(java.lang.String, java.lang.String); flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=3 0: new #16 // class java/lang/StringBuilder 3: dup 4: aload_1 5: invokespecial #24 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 8: aload_2 9: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 12: invokevirtual #31 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 15: areturn LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 16 0 this Ltest/Java; 0 16 1 s1 Ljava/lang/String; 0 16 2 s2 Ljava/lang/String;
看结果,相比自动优化,减少了一个invokestatic指令的执行,编译后的字节码占用也少3个字节。
android
客户端android,jvm是基于寄存器的,那编译后会是怎么样呢?dexdump工具闪亮登场
android在编译时,是把.class文件再转换为dex文件,所有的class都合并一起,这带来节约空间,没有服务端那样每个class就一个很大的常量池,但其方法数是用short来计数的,这也带来了64k问题,android也提供mutildex解决方法,跑题了,dexdump就是对class.dex进行解析。
+拼接
#0 : (in Lcom/sunqi/service/Java;) name : 'test' type : '(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;' access : 0x0001 (PUBLIC) code - registers : 5 ins : 3 outs : 2 insns size : 18 16-bit code units007578: |[007578] com.sunqi.service.Java.test:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;007588: 2200 6d00 |0000: new-instance v0, Ljava/lang/StringBuilder; // type@006d00758c: 7110 3001 0300 |0002: invoke-static {v3}, Ljava/lang/String;.valueOf:(Ljava/lang/Object;)Ljava/lang/String; // method@0130007592: 0c01 |0005: move-result-object v1007594: 7020 3201 1000 |0006: invoke-direct {v0, v1}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@013200759a: 6e20 3501 4000 |0009: invoke-virtual {v0, v4}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@01350075a0: 0c00 |000c: move-result-object v00075a2: 6e10 3601 0000 |000d: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@01360075a8: 0c00 |0010: move-result-object v00075aa: 1100 |0011: return-object v0 catches : (none) positions : 0x0000 line=5 locals : 0x0000 - 0x0012 reg=2 this Lcom/sunqi/service/Java; 0x0000 - 0x0012 reg=3 s1 Ljava/lang/String; 0x0000 - 0x0012 reg=4 s2 Ljava/lang/String;
大体结构与之前有些类似,name标识方法名,type标识入参,access是访问权限,positions对应代码行,locals对应局部变量,只是里面使用寄存器。
registers:寄存器数,因为移动端主要是基于arm的cpu架构,RISC采用寄存器来实现,关于栈和寄存器哪种实现好,优缺点是什么,本文不作讨论,具体大家可以去google一下stack vs registers代码编译后的指令,与服务端则也有点类似,只是指令已经不同,具体请参考https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html
stringbuilder拼接
#1 : (in Lcom/sunqi/service/Java;) name : 'test1' type : '(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;' access : 0x0001 (PUBLIC) code - registers : 4 ins : 3 outs : 2 insns size : 14 16-bit code units0075ac: |[0075ac] com.sunqi.service.Java.test1:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;0075bc: 2200 6d00 |0000: new-instance v0, Ljava/lang/StringBuilder; // type@006d0075c0: 7020 3201 2000 |0002: invoke-direct {v0, v2}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@01320075c6: 6e20 3501 3000 |0005: invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@01350075cc: 0c00 |0008: move-result-object v00075ce: 6e10 3601 0000 |0009: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@01360075d4: 0c00 |000c: move-result-object v00075d6: 1100 |000d: return-object v0 catches : (none) positions : 0x0000 line=9 locals : 0x0000 - 0x000e reg=1 this Lcom/sunqi/service/Java; 0x0000 - 0x000e reg=2 s1 Ljava/lang/String; 0x0000 - 0x000e reg=3 s2 Ljava/lang/String;
对比直接+拼装的字节码,可以看到直接用stringbuilder拼装少执行两个指令,同时也只要4个寄存器就够了。
不过比较java服务端的字节码和android字节码,发现机遇寄存器的明显使用更少的指令即可以实现功能。基于栈实现需要更多的指令和内存访问,这也是移动端采用寄存器架构的原因之一吧
总结:
1、我们需要了解,底层的实现到底是怎么样的,不同的平台有不同特点
2、有时候需要从另一个视角看我们写的代码,在机器上运行会是怎么样,以便于更好的写代码
感谢 分享 http://sunqi.iteye.com/blog/2273373
【腾讯内部干货分享】分析Dalvik字节码进行减包优化 http://wetest.qq.com/lab/view/?id=96&from=ads_test2_qqtips&sessionUserType=BFT.PARAMS.193009.TASKID&ADUIN=532007154&ADSESSION=1466646757&ADTAG=CLIENT.QQ.5467_.0&ADPUBNO=26558
- 从字节码视角看java字符串的拼接
- 从JAVA字节码看++i 和i++ 的区别
- 从狼的视角看世界
- 从数学的视角看社交网络
- 从电信运营商的视角看物联网的发展
- 从代码的视角看DOS时代的通配符
- 从六个视角 看 电脑遥控器的市场前景
- 从后现代主义视角看当下的Linux文化
- 从CDN视角看公共DNS的选择
- 从业务视角看交互设计师的价值
- 从CNN视角看在自然语言处理上的应用
- 从CNN视角看在自然语言处理上的应用
- 从CNN视角看在自然语言处理上的应用
- 从CNN视角看在自然语言处理上的应用
- 从CNN视角看在自然语言处理上的应用
- 从业务视角看交互设计师的价值
- 从字节码角度看String的连接操作
- (5)java 字符串 从源代码的角度聊聊java中StringBuffer、StringBuilder、String中的字符串拼接
- python 中execl等表格操作学习心得
- BZOJ1054 [HAOI2008]移动玩具
- 软件测试理论与经验--阅读笔记
- [疯狂Java]面向对象:命名规范、重载、值传递、可变参数、static/this
- 在web页面上快速生成二维码的三种实用方法
- 从字节码视角看java字符串的拼接
- 最流行的android组件大全
- 四大组件之BroadcastReceiver(一)-自定义“收音机”与发送“广播”
- springmvc 分页查询的简单实现
- detect_indent_fft.hdev
- 第十六周--二进制文件浏览器
- PropertyAnimator学习之ObjectAnimator
- Sigar介绍与使用
- Java中的数据存储(堆及堆栈)