导言
大家好,我是南橘,从接触java到现在也有差不多两年时间了,两年时间,从一名连java有几种数据结构都不懂超级小白,到现在懂了一点点的进阶小白,学到了不少的东西。知识越分享越值钱,我这段时间总结(包括从别的大佬那边学习,引用)了一些平常学习和面试中的重点(自我认为),希望给大家带来一些帮助
有需要的同学可以加我的公众号,以后的最新的文章第一时间都在里面,也可以找我要思维导图
上一章介绍了字符串、数字和集合类的一些高效用法,这一章就继续查漏补缺、介绍更多的性能优化技巧。
一、int转String的用法
从之前的文章可以得知,int到String的转换是一个耗时的操作,因为我们需要尽量避免做这些转换。如果实在需要,也可以动用上期Integer自动拆包装包的方法,预先将一部分的int值转化为字符串。
我们通过工具类,预先设定好1024个缓存(或者根据业务设置更多),所有调用int2String方法的时候都会预先判断数据是否在缓存内,如果小于1024,则会去调用缓存数据。
测试代码如下,我应该会在下下一章详细介绍我如何通过JMH来对代码性能进行测试
*
大家注意一下这三张图片Benchmark区域的信息不难发现,在数字1024以内,使用缓存的int2StringByCache的性能几乎高出int2String一个数量级。
这里就是运用了JDK对INTEGER自动拆箱装箱的原理
二、使用Native方法
Native方法就是调用一个非Java代码的接口。
我在之前的文章【进阶之路】攻克JVM——JVM的垃圾回收机制(二)里有讲过。
一般来说,作为java的底层代码,Native有着更好的性能。
最常用的Native方法是Stream.arraycopy方法,把原数组的内容复制到目标数组中:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
里面还有各种各样的方法,大家可以根据情况来使用。
三、Switch优化
在条件判断中,如果有较多的分支判断,使用switch语句通常比使用if语句的效率更高。if语句会每次取出变量进行比较从而确定处理分支,而switch语句只需要取出一次变量,然后根据tableswitch直接找到分支即可。
根据测试,分支较少的情况下,if和switch的速度差不多,再分支较多的情况下,switch的速度就快多了。
这里大家可以自己测试一下。
我们知道JDK1.8之后,JDK支持String类型,是因为在变异的时候,使用hashCode来作为switch的实际值。
首先写一段switch结构的代码:
public static void main(String[] args) {
final String str = "C";
switch (str) {
case "A" :
System.out.println("A");
break;
case "B" :
System.out.println("B");
break;
default:
System.out.println("C");
}
}
public static void main(String[] args) {
String str = "C";
String var2 = "C";
byte var3 = -1;
switch(var2.hashCode()) {
case 65:
if (var2.equals("A")) {
var3 = 0;
}
break;
case 66:
if (var2.equals("B")) {
var3 = 1;
}
}
switch(var3) {
case 0:
System.out.println("A");
break;
case 1:
System.out.println("B");
break;
default:
System.out.println("C");
}
}
可以看到,switch结构中变为了String.hashcode()方法,利用其返回的int值进行判断,所以说编译后还是使用了switch(int)结构来实现的。而且我们知道,String的hashcode方法是有哈希冲突的风险的,所以我们应该在每个case条件中增加了equals作为补充判断,避免哈希冲突错误。
四、优先使用局部变量
当存取类变量的时候,Java使用的是虚拟机指令GETFFIELD获取类变量,如果存取方法的变量,则通过出栈操作获取变量。
在之前的文章【进阶之路】攻克JVM——JVM对象及对象的访问定位(一)里有提到
JVM提出栈上分配的概念,针对作用域在方法内的对象,如果满足了逃逸分析,就会将对象属性打散后分配在栈上(线程私有的,属于栈内存),这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给GC增加额外的无用负担,从而提升应用程序整体的性能。
因此在需要频繁操作类变量的时候,最好先赋值给一个局部变量,比如这样:
int[] arr = new int[1024];
for (int i=0;i<1024;i++){
arr[i]=i;
}
但是在实际的开发过程中,由于CPU缓存的原因,并不是每次都从Heap(堆)中取出变量,会从CPU缓存中存取,所以在JMH测试中难以验证谁的性能更好,这边我就不展示了。但是,通过对JVM机制的学习,我们能清楚的知道局部变量的好处。
五、预处理、预分配、预编译
1、预处理
预处理指的是对于需要反复调用的代码,可以尝试取出公共的只读代码块,处理一次生成并保留处理结果。这样接下来需要反复调用的时候,可以直接引用处理的结果。
随便举个例子,假设是黑名单判断,没有做预处理的代码如下:
ServiceConfig serviceConfig =new ServiceConfig("a,A,S,DA,ASD,F,SDF,SD,F,F,A,D,F");
Set<String> strings = serviceConfig.getblackService();
forbid =strings.contains("a");
然后稍微改一下,这里是做了预处理的
public class ServiceConfig{
String black=null;
Set<String> blackSet =new HashSet<>();
public ServiceConfig(String black){
this.black=black;
this.blackSet.addAll(Arrays.asList(black.split(",")));
}
public Set<String> getblackService(){
return blackSet;
}
}
ServiceConfig serviceConfig =new ServiceConfig("a,A,S,DA,ASD,F,SDF,SD,F,F,A,D,F");
Set<String> strings = serviceConfig.getblackService();
forbid =strings.contains("a");
然后放在测试环境一跑,大家看的很明显了,谁才是版本答案:
2、预分配
预分配就很简单了,JDK存在大量预先分配空间的代码,比如我上一章讲的StringBuilder,会初始分配一段空间,不必每次调用append时才分配。
每次在append之前,也会检查分配空间是否足够,如果足够,则不需要增加空间。
如果所有的业务代码都能预分配合理的空间,那么系统的业务性能也会有合理的提高。
3、预编译
JDBC在处理SQL语句时有一个预编译的过程,而预编译对象就是把一些格式固定的SQL编译后,存放在内存池中即JDBC缓冲池,当我们再次执行相同的SQL语句时就不需要预编译的过程了,所以即使SQL注入特殊的语句,也会只当做参数传进去,不会当做指令执行。这个功能一大优势就是能提高执行速度,尤其是多次操作数据库的情况,再一个优势就是预防SQL注入,严格的说,应该是预防绝大多数的SQL注入。
还有种用法,涉及到格式化、序列化的时候,预编译成长红箭格式是一种提高性能的办法,比如在日志输出的时候可能会采用这种方法,
String类的format()方法用于创建格式化的字符串以及连接多个字符串对象,会解析format中出现的“{}”符号
(类似这样)
预编译的方式也能提升服务的性能。
结语
我们在编写代码的过程中,稍稍一注意,就能全面提升代码的性能。这一次的系列文章也是出于这个角度所编写的,接下来我会继续的思考和查阅资料,进一步完善调优系列。
同时需要思维导图的话,可以联系我,毕竟知识越分享越香!