搞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 units
007578: |[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@006d
00758c: 7110 3001 0300 |0002: invoke-static {v3}, Ljava/lang/String;.valueOf:(Ljava/lang/Object;)Ljava/lang/String; // method@0130
007592: 0c01 |0005: move-result-object v1
007594: 7020 3201 1000 |0006: invoke-direct {v0, v1}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@0132
00759a: 6e20 3501 4000 |0009: invoke-virtual {v0, v4}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@0135
0075a0: 0c00 |000c: move-result-object v0
0075a2: 6e10 3601 0000 |000d: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@0136
0075a8: 0c00 |0010: move-result-object v0
0075aa: 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 units
0075ac: |[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@006d
0075c0: 7020 3201 2000 |0002: invoke-direct {v0, v2}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@0132
0075c6: 6e20 3501 3000 |0005: invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@0135
0075cc: 0c00 |0008: move-result-object v0
0075ce: 6e10 3601 0000 |0009: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@0136
0075d4: 0c00 |000c: move-result-object v0
0075d6: 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、有时候需要从另一个视角看我们写的代码,在机器上运行会是怎么样,以便于更好的写代码
相关推荐
java 字符串转16进制 16进制转字符串 将两个ASCII字符合成一个字节; java 字符串转16进制 16进制转字符串 将两个ASCII字符合成一个字节; java 字符串转16进制 16进制转字符串 将两个ASCII字符合成一个字节; java ...
这意味着在字符串之间进行拼接,你需要将字符串转换为字节,使用字节拼接的手段,然后再将结果转换回字符串。例如,可以通过`bytes`类型将字符串转换为字节数组,并用加号`+`运算符将字节拼接起来。但要注意,在...
在Java编程中,字符串拼接是一项常见的操作,特别是在构建动态字符串或者处理文本数据时。本文主要探讨了在Java中几种不同的字符串拼接方法的性能差异,包括使用操作符`+`、`String.concat()`、`StringBuffer.append...
根据提供的文件信息,本文将详细解释Java中字符串的不同编码转换方法及原理,并深入探讨每种编码格式的特点。 ### Java字符串的编码转换 在Java中,处理不同字符集之间的字符串转换是一项常见任务。尤其是在处理...
Java字符串长度不够,将其前面补0或者后面补0,适用于字典序比较前,将要比较字符串的位数保持一致,再进行比较。
### Java字符串内存计算 在Java开发中,理解内存管理至关重要,特别是对于字符串处理而言。本文将深入探讨如何在Java中计算字符串所占用的内存空间,包括现有的计算方法、其局限性以及具体的计算公式。 #### 计算...
在Java编程语言中,字符串是极其重要且常用的数据类型,尤其在Android开发中更是不可或缺。字符串主要用于处理文本信息,如用户输入、文件内容、网络数据等。以下是对"JAVA 字符串应用笔记"中可能涉及的一些核心知识...
- `getBytes()`: 将字符串转换为字节数组,使用平台默认的字符集。 - `charAt(int index)` 和 `codePointAt(int index)`: 获取指定位置的字符,`codePointAt`适用于处理Unicode字符。 8. **创建子串** - `...
本文将深入探讨“JAVA可逆带秘钥字符串加密算法”,这是一种允许数据在加密后仍能通过正确的密钥解密回原始形式的加密技术。 在Java中,最常见的可逆加密算法包括DES(Data Encryption Standard)、3DES(Triple ...
Java 字符串的拼接是编程中常见的操作,尤其在构建复杂的字符串格式或组合信息时。在Java中,字符串对象是不可变的,这意味着一旦创建,就不能改变其内容。这特性源于String类的设计,它是final类,且其方法都不会...
JAVA字符串拼接常见方法汇总 Java字符串拼接是Java编程中一个非常常见的操作,但是实现字符串拼接的方法却有多种。下面我们将详细介绍Java字符串拼接常见方法,并分析每种方法的优缺点。 一、使用“+”号连接字符...
2. **从字符数组构造**:`String(char[] chars)` 可以通过一个字符数组来初始化字符串,例如: ```java char chars[] = {'j', 'a', 'v', 'a'}; String s1 = new String(chars); ``` 3. **从字符数组的子集构造**...
C#字节数组转16进制字符串 C#字节数组转16进制字符串 QQ:292258449
`getChars()` 方法用于从字符串中复制字符到目标字符数组。 - **语法**: - `void getChars(int sourceStart, int sourceEnd, char[] target, int targetStart)`:将当前字符串中从`sourceStart`到`sourceEnd - 1`...
在Java编程语言中,字符串(String)是至关重要的数据类型,用于处理文本信息。字符串是不可变的,这意味着一旦创建,就不能更改其内容。本实例源码集主要关注Java中的字符串和文本处理,提供了多种实际应用的示例,...
在Java编程语言中,处理带有汉字的字符串时,由于汉字占据多个字节,按照字节进行截取可能会导致汉字被不完整地分割,从而产生乱码。为了解决这个问题,我们需要理解Unicode编码以及如何在Java中正确处理多字节字符...
在C#编程中,有时我们需要根据字节长度来截取字符串,这可能是因为要处理不同编码格式的数据,或者为了适应特定的传输限制。本篇将详细介绍如何在C#中按照字节长度截取字符串,并结合正则表达式进行操作。 首先,...
如果`nIndex`超出字符串末尾,则从字符串末尾插入。 9. **Remove()**: 移除字符串中所有指定的字符。 ```cpp csStr = "aabbaacc"; csStr.Remove('a'); cout ; // 输出:bbcc ``` 通过以上介绍,我们可以...
此方法可以从字符串中提取子字符串。 ```java String sub = "hello world".substring(6); // sub 的值为 "world" ``` #### 13. 字符串连接:`concat()` 此方法可以将两个字符串连接在一起。 ```java String result...
### Java字符串编码转换详解 #### 一、Java 字符串编码转换基础 在Java中,字符串的处理是非常常见的操作之一,而字符编码是确保数据正确显示的关键因素。本篇文章将重点介绍Java中字符串编码的转换方法及其在Web...