代码优化:在现代服务器环境下提升运行效率的关键策略

来源 |个人博客|作者 |五月仓颉

代码优化:在现代服务器环境下提升运行效率的关键策略

本文作者:五月仓颉结合自己的工作和日常学习经验,重新谈谈为什么需要代码优化。修改前,作者声明如下:

就像鲸鱼吃虾一样,也许吃一两只虾对鲸鱼来说并没有多大作用,但如果吃更多的虾,鲸鱼自然就吃饱了。

代码优化也是一样。也许一两次优化对于提高代码的运行效率来说意义不大,但是只要能处处注重代码优化,总体来说对于提高代码的运行效率是非常有用的。

这种观点,在目前看来,是代码优化的一个理由,但并不完全正确。在机械技术发展的今天,服务器往往有8核、16核、64位CPU,代码执行效率非常高。 StringBuilder 取代了 StringBuffer,ArrayList 取代了 Vector。代码运行效率的提升是微乎其微的,即使项目中的每一项我都注意到了,并且在运行代码时没有看到明显的变化。

在我看来,代码优化最重要的作用应该是避免未知的错误。线上运行代码的过程中,经常会出现很多意想不到的错误,因为线上环境和开发环境有很大的不同,而错误最终往往定位到一个很小的原因。不过,为了解决这个错误,我们需要先进行自我验证,然后打包要替换的class文件,暂停业务并重新启动。对于一个成熟的项目来说,最后一个其实有非常大的影响,这意味着用户在这段时间无法访问应用程序。因此,在编写代码时,从源头上关注各种细节,权衡并使用最佳选项,这将在很大程度上避免未知的错误,从长远来看也会大大减少工作量。

1. 代码优化的目标是:

1. 减少代码大小

2.提高代码运行效率

本文内容有的来自网络,有的来自日常工作学习。当然,这并不重要。重要的是这些代码优化的细节是否真的有用。这篇文章会长期更新。只要有值得分享的代码优化细节,本文就会不定期更新。

2.代码优化细节:

1、尽量指定类和方法的final修饰符。

带有final修饰符的类不会被派生。在Java核心API中,有很多final应用的例子,比如java.lang.String,整个类都是final的。为类指定final修饰符可防止该类被继承,为方法指定final修饰符可防止该方法被重写。如果一个类被指定为final,则该类的所有方法都是final的。 Java 编译器将寻找机会内联所有最终方法。内联对于提高Java运行效率有着显着的作用。有关详细信息,请参阅 Java 运行时优化。这可以平均提高 50% 的性能。

2. 尽可能重复使用对象

特别是对于String对象的使用,在发生字符串连接时应该使用StringBuilder/StringBuffer来代替。由于Java虚拟机不仅花时间生成对象,将来可能还需要花时间垃圾收集和处理这些对象。因此,生成过多的对象会对程序的性能产生很大的影响。

3.尽可能使用局部变量

调用方法时传递的参数以及调用过程中创建的临时变量都保存在栈中,速度更快。其他变量,如静态变量、实例变量等都是在堆中创建的,速度较慢。此外,在堆栈上创建的变量的内容随着方法结束而消失,并且不需要额外的垃圾收集。

4、及时关闭流量

在Java编程中,进行数据库连接和I/O流操作时必须小心。使用完毕后及时关闭,释放资源。因为操作这些大对象会造成很大的系统开销,稍有不慎就会导致严重的后果。

5. 尽量减少变量的双重计算

澄清一个概念,调用一个方法,即使方法中只有一条语句,其代价也是高昂的,包括创建栈帧、调用方法时保护场景、调用方法时恢复场景等。所以对于例如以下操作:

建议将其替换为:

这样当list.size()很大的时候,就减少了很多消耗。

6、尽量采用懒加载策略,即只在需要的时候创建。

例如:

建议将其替换为:

7. 谨慎使用异常

异常不利于性能。要抛出异常,必须首先创建一个新对象。 Throwable 接口的构造函数调用名为 fillInStackTrace() 的本地同步方法。 fillInStackTrace() 方法检查堆栈并收集调用跟踪信息。每当抛出异常时,Java虚拟机必须调整调用堆栈,因为在处理过程中会创建一个新对象。异常应该只用于错误处理,而不应该用于控制程序流程。

8.不要在循环中使用try...catch...。它应该放在最外层。

根据网友提出的意见,我认为这是值得讨论的。

9. 如果要添加的内容的长度可以估计,请为底层集合和以数组方式实现的工具类指定初始长度。

例如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等,以StringBuilder为例:

可以通过类的构造函数(这里指的不仅仅是上面的StringBuilder)来设置其初始化能力,这样可以显着提高性能。例如StringBuilder,length表示当前StringBuilder可以容纳的字符数。因为当StringBuilder达到其最大容量时,它会将其容量增加到当前容量的2倍加上2。每当StringBuilder达到其最大容量时,它必须创建一个新的字符数组,然后用它替换旧的字符。数组的内容被复制到一个新的字符数组中 - 这是一个非常消耗性能的操作。试想一下,如果在不指定长度的情况下,可以估计字符数组中会存储大约5000个字符,最接近的2到5000的幂是4096,每次扩展都加2,那么:

在4096的基础上,申请8194个字符数组,加起来就是一次性申请12290个字符数组。如果一开始就可以指定5000个字符数组,那么将节省一倍以上的空间;

将原来的4096个字符复制到新的字符数组中;

这样既浪费了内存空间,又降低了代码运行效率。因此,为底层集合和用数组实现的工具类设置合理的初始化容量是没有错的,会带来立竿见影的效果。但请注意,对于像HashMap这样以数组+链表实现的集合,不要将初始大小设置为与你估计的大小相同,因为一张表只连接一个对象的可能性几乎为0。建议初始大小设置为2的N次方。如果可以估计有2000个元素,可以设置为new HashMap(128)或者new HashMap(256)。

10、复制大量数据时,使用System.arraycopy()命令

11.乘法和除法使用移位运算

例如:

使用移位操作可以大大提高性能,因为在计算机底层,位操作是最方便、最快的,所以建议修改为:

移位操作虽然很快,但是可能会导致代码难以理解,所以最好添加相应的注释。

12.不要在循环内不断创建对象引用

例如:

这种方法会导致内存中存在count个Object对象引用。如果计数很大,就会消耗内存。建议改为:

在这种情况下,内存中只有一份 Object 对象引用的副本。每次调用new Object()时,Object对象引用都指向不同的Object,但内存中只有一份,这样就大大节省了内存空间。

13、基于效率和类型检查的考虑,尽可能使用数组,只有在无法确定数组大小的情况下才使用ArrayList。

14. 尝试使用HashMap、ArrayList、StringBuilder。除非线程安全需要,否则不建议使用Hashtable、Vector、StringBuffer。后三者由于使用同步机制而导致性能开销。

15. 不要将数组声明为 public static final

因为这是没有意义的。这仅将引用定义为静态最终。数组的内容仍然可以随意改变。将数组声明为公共是一个安全漏洞,这意味着该数组可以被外部类更改。

16.在适当的情况下尝试使用单例。

使用单例可以减轻加载负担,缩短加载时间,提高加载效率,但并不是所有地方都适用单例。简单来说,单例主要适合以下三个方面:

(1)控制资源的使用,通过线程同步控制资源的并发访问;

(2)控制实例的生成,达到节省资源的目的;

(3) 控制数据的共享,使多个不相关的进程或线程之间能够进行通信,而无需建立直接关联;

17.尽量避免随意使用静态变量。

要知道,当一个对象被定义为static的变量引用时,gc通常不会回收这个对象占用的堆内存,如:

此时静态变量b的生命周期与A类相同,如果A类没有被卸载,引用B指向的B对象就会驻留在内存中,直到程序终止。

18、及时清理不再需要的会话

为了清除不再活动的会话,许多应用程序服务器都有默认的会话超时,通常为 30 分钟。当应用服务器需要保存更多会话时,如果内存不足,操作系统会将部分数据转移到磁盘上。应用服务器还可能根据MRU(最近使用的)算法将一些不活动的会话转储到磁盘。甚至可能会引发内存不足异常。如果要将会话转储到磁盘,则必须首先对其进行序列化。在大规模集群中,序列化对象的成本很高。因此,当不再需要session时,应及时调用HttpSession的invalidate()方法来清除session。

19. 实现RandomAccess接口的集合,例如ArrayList,应该使用最常见的for循环而不是foreach循环来遍历。

这是JDK向用户推荐的。 JDK API对RandomAccess接口的解释是:实现RandomAccess接口用于表明支持快速随机访问。该接口的主要目的是允许通用算法改变其行为,从而在应用于随机或连续访问列表时提供良好的性能。表现。实践经验表明,如果随机访问实现RandomAccess接口的类实例,使用普通的for循环会比使用foreach循环更高效;相反,如果顺序访问,使用Iterator会更高效。可以使用类似下面的代码来进行判断:

foreach循环的底层实现原理是迭代器Iterator,参见Java语法糖1:变长参数和foreach循环原理。所以后半句“另一方面,如果是顺序访问的话,使用Iterator会更高效”的意思就是那些顺序访问的类实例是使用foreach循环来遍历的。

20.使用同步代码块而不是同步方法

这一点在多线程模块中synchronized锁方法块一文中已经讲得很清楚了。除非确定整个方法需要同步,否则尽量使用同步代码块,避免出现不需要同步的代码。还要进行同步,影响代码执行效率。

21、将常量声明为staticfinal,并以大写字母命名

这样就可以在编译时将这些内容放入常量池中,避免运行时计算常量的值。另外,用大写字母命名常量也可以更容易区分常量和变量。

22.不要创建一些未使用的对象,也不要导入一些未使用的类。

这是没有意义的。如果代码中出现“局部变量i的值没有被使用”或者“The import java.util is neverused”,请删除这些无用的内容。

23、程序运行时避免使用反射

请参阅反射。反射是Java为用户提供的一个非常强大的功能。功能强大往往意味着效率低下。程序运行过程中不建议使用反射机制,尤其是频繁使用反射机制的,尤其是Method的invoke方法。如果确实有必要,推荐的做法是对项目启动时需要通过反射加载的类使用反射实例。创建一个对象并将其放入内存中——用户只关心与同伴交互时获得最快的响应,而不关心同伴的项目需要多长时间才能启动。

24、使用数据库连接池和线程池

两个池都用于重用对象。前者可以避免频繁打开和关闭连接,后者可以避免频繁创建和销毁线程。

25.使用缓冲输入和输出流进行IO操作

缓冲输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,可以大大提高IO效率

26、顺序插入和随机访问较多的场景使用ArrayList,元素删除和中间插入较多的场景使用LinkedList。

这个通过了解ArrayList和LinkedList的原理就知道了。

27.公共方法中不要有太多形式参数

公共方法是提供给外界的方法。如果给这些方法赋予太多的形式参数,则有两个主要缺点:

(1)违背了面向对象的编程思想。 Java强调一切皆对象。形式参数过多,不符合面向对象编程思想;

(2)参数过多必然导致方法调用的错误概率增加;

至于“太多”指的是多少,我猜是3个或者4个。比如我们用JDBC写了一个insertStudentInfo方法。有 10 个学生信息字段需要插入到 Student 表中。这10个参数可以封装在一个实体类中作为insert方法的形参。

28、当字符串变量和字符串常量相等时,将字符串常量写在前面。

这是一个比较常见的技巧。如果您有以下代码:

建议修改为:

这样做主要是为了避免空指针异常

29.请注意,Java中if(i==1)和if(1==i)没有区别,但是从阅读习惯来看,建议使用前者

人们常常会问,“if(i==1)”和“if(1==i)”有什么区别吗?这要从 C/C++ 开始。

在C/C++中,“if(i==1)”判断条件是根据0和非0成立的。 0 表示假,非 0 表示真。假如有这样一段代码:

C/C++判断“i==1”不为真,因此表示为0,为假。但如果:

如果程序员不小心写成了“if(i==1)”而不是“if(i=1)”,就会出现问题。在 if 中将 i 赋值为 1。如果判断里面的内容不为0,则返回值为true。但是很明显i是2,比较的值是1,所以应该返回false。这种情况在C/C++开发中很可能出现,并且会导致一些难以理解的错误。因此,为了避免开发者在if语句中进行错误的赋值操作,建议将if语句写成:

这样,即使开发人员不小心写了“1 = i”,C/C++编译器也可以立即检查出来,因为我们可以将i的值赋给变量,但不能将1的值赋给常量。

然而在Java中,C/C++的“if(i=1)”语法是不可能的,因为一旦写出这个语法,Java就会编译并报错“类型不匹配:无法从int转换为boolean”。但是,虽然Java的“if(i==1)”和“if(1==i)”在语义上没有区别,但从阅读角度来看,还是用前者更好。

30. 不要在数组上使用 toString() 方法

看一下在数组上使用 toString() 打印的内容:

初衷是打印出数组的内容,但是由于数组引用为空,可能会导致空指针异常。不过,虽然toString()对于数组来说没有意义,但是对于集合来说toString()却可以打印出集合的内容,因为集合的父类AbstractCollections重写了Object的toString()方法。

31、不要对超出范围的基本数据类型进行向下强制转换。

这永远不会给出期望的结果:

我们可能期望得到其中一些,但结果是:

解释。 Java中的Long是8字节64位,所以12345678901234在计算机中的表示应该是:

0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010

int类型数据有4个字节,32位。上述一串二进制数据的前32位取低位为:

0111 0011 1100 1110 0010 1111 1111 0010

该字符串的二进制表示形式为十进制 1942892530,因此这就是上面控制台上的输出内容。从这个例子可以得出两个结论:

(1) 整数默认数据类型为int,long l = 12345678901234L。这个数字已经超出了int的范围,所以末尾有一个L,表明这是一个long数字。顺便说一句,浮点类型的默认类型是double,所以定义float时应该写“”float f = 3.5f”

(2)接下来如果写“int ii = l + i;”,就会报错,因为long + int是long,不能赋值给int。

32.公共集合类中未使用的数据必须及时移除

如果一个集合类是public的(即它不是方法中的属性),那么集合中的元素将不会被自动释放,因为总有引用指向它们。因此,如果公共集合中的某些数据没有被使用并且没有被删除,那么公共集合将会不断增长,导致系统存在内存泄漏的可能性。

33、将基本数据类型转换为字符串,基本数据类型.toString()是最快的方式,String.valueOf(data)次之,data+""最慢。

基本数据类型转换通常有三种方法。我有一个整数数据i。我可以使用 i.toString()、String.valueOf(i) 和 i+""。这三种方法的效率如何?参见测试:

运行结果为:

因此,以后遇到基本数据类型转换为String时,优先使用toString()方法。至于为什么,也很简单:

(1)String.valueOf()底层方法调用了Integer.toString()方法,但调用前会做一个简短的判断;

(2)Integer.toString()方法我就不讲了,直接调用即可;

(3)i+""底层使用StringBuilder实现,先使用append方法拼接,然后使用toString()方法获取字符串;

比较三者,很明显2最快,1第二,3最慢。

34.用最有效的方式遍历地图

遍历地图的方法有很多种。一般场景下,我们需要的是遍历Map中的Keys和Values。推荐且最有效的方法是:

如果只是想迭代这个Map的键值,那么使用“Set keySet = hm.keySet();”会更合适。

35、建议单独操作资源的close()。

意思是,例如,我有这段代码:

建议修改为:

虽然有点麻烦,但是可以避免资源泄漏。我们认为如果没有修改代码的话,如果XXX.close()抛出异常,那么就会进入catch块,YYY.close()不会被执行,YYY资源也不会被回收。像这样占用过多的代码可能会导致资源句柄泄漏。改成下面的写法后,保证XXX和YYY无论如何都会被关闭。

36、使用前或使用后一定要删除ThreadLocal。

目前基本上所有的项目都采用了线程池技术,这是非常好的。线程数量可以动态配置,线程可以复用。

但是,如果你的项目中使用了ThreadLocal,请务必在使用之前或之后将其删除。这是因为上面提到的线程池技术是线程复用,也就是说代码运行时,某个线程不会被销毁而是会等待下次使用。我们看一下Thread类中持有ThreadLocal.ThreadLocalMap的引用:

线程没有被销毁意味着前一个线程设置的ThreadLocal.ThreadLocalMap中的数据仍然存在。那么当下一个线程复用这个Thread时,很可能拿到的是上一个线程设置的数据而不是你想要的内容。

这个问题非常晦涩难懂。一旦因为这个原因出现错误,如果没有相关经验或者没有扎实的基础,是很难发现这个问题的。所以写代码的时候要注意这一点,这样以后你的工作量会减少很多。

37.记住使用常量定义而不是魔鬼数字。魔鬼数的存在会大大降低代码的可读性。是否应该使用常量来定义字符串常量可以根据具体情况而定。

38.当long或Long初始赋值时,使用大写的L而不是小写的l,因为字母l很容易与数字1混淆。这一点非常详细,值得注意。

39.所有重写的方法必须保留@Override注解

原因有以下三个:

(1)明确这个方法是从父类继承的;

(2) getObject()和get0object()方法,前者的第四个字母是“O”,后者的第四个字母是“0”。添加@Override注解可以立即判断重写是否成功;

(3)修改抽象类中的方法签名,实现类会立即报编译错误;

40、建议使用JDK7中新引入的Objects工具类来进行对象的equals比较。直接a.equals(b),存在空指针异常的风险。

41、循环体中不要使用“+”进行字符串拼接,而是直接使用StringBuilder进行连续追加。

先说一下字符串拼接不使用“+”的原因。如果我有一个方法:

这段代码编译后使用javap -c反编译.class文件,截取关键部分:

这意味着虚拟机每次遇到“+”操作符拼接字符串时,都会创建一个新的StringBuilder,然后调用append方法,最后调用toString()方法将字符串转换并赋值给oriStr对象,即循环多少次,就会生成多少个new StringBuilder(),比较浪费内存。

42、不要捕获Java类库中定义的继承自RuntimeException的运行时异常类

异常处理效率低。 RuntimeException的大部分运行时异常类都是程序员可以避免的,比如:

(1)通过判断除数是否为空可以避免ArithmeticException;

(2)通过判断对象是否为空可以避免NullPointerException;

(3)通过判断数组/字符串的长度可以避免IndexOutOfBoundsException;

(4)可以通过instanceof关键字规避ClassCastException;

(5) 使用迭代器可以避免ConcurrentModificationException;

43.避免Random实例被多个线程使用。虽然共享此实例是线程安全的,但会因竞争同一种子而导致性能下降。 JDK7之后,可以使用ThreadLocalRandom来获取随机数。

解释同一种子的竞争导致性能下降的原因。例如,看一下 Random 类的 nextInt() 方法的实现:

调用 next(int bits) 方法,这是一个受保护的方法:

这里的种子是一个全局变量:

当多个线程同时获取随机数时,它们会竞争同一个种子,导致效率降低。

44、静态类、单例类和工厂类将其构造函数设置为私有

这是因为静态类、单例类和工厂类不需要外部new。将构造函数设置为私有可确保这些类不会生成实例对象。

后记

优秀的代码来自于每一个小小的优化。关注每一个细节不仅可以提高程序的运行效率,还可以避免很多未知的问题。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读