在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。
1. 尽量重用对象,不要循环创建对象,比如:for 循环字符串拼接(不在 for中使用String类型 + 拼接,因为其底层是先new 一个StringBuilder再在 for 里 append最后调用StringBuilder的toString()方法 ). 由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响 , 出现字符串连接情况时应使用StringBuffer代替
StringBuilder s=new StringBuilder();
for(int i=0;i<10;i++){
s.append("C");
}
2. 容器类初始化的地时候指定大小(长度)
List<String> collection = new ArrayLIst<String>(5);
Map<String, String> map = new HashMap<String, String>(32);
StringBuffer buffer = new StringBuffer(16);
3. ArrayList(底层数组)随机遍历快,LinkedList(底层双向链表)添加删除快
4. 集合遍历尽量减少重复计算,改成
for (int i = 0,size=list.size();i<size; i++) {
}
5. 使用键值对Entry 遍历 Map
for(Map.Entry<String,String> entry:map.entrySet()){
String key =entry.getkey();
String value=entry.getValue();
}
6. 大数组复制使用System.arraycopy
int[] is = new int[]{1,2,3,4,5};
int[] is2 = new int[is.length];
System.arraycopy(is, 0, is2, 0, is.length);
System.out.println(Arrays.toString(is2));
7. 尽量使用基本类型而不是包装类型,避免自动拆装箱 空指针问题
尽量使用基本数据类型作为方法参数类型 , 尽量使用基本数据类型作为方法返回值类型
Integer i = 10;
Integer j = 20;
System.out.println(i+j);
//反编译后代码如下:
Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(20);
System.out.println(i.intValue() + j.intValue());
8. 不要手动调用 System.gc()
9. 及时消除过期对象的引用,防止内存泄漏
10. 尽量使用局部变量,调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。
11. 尽量使用尽量使用非同步的容器HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销
StringBuffer,StringBuilder的区别在于:java.lang.StringBuffer 线程安全的可变字符序列。一个类似于String的字符串缓冲区,但不能修改。StringBuilder与该类相比,通常应该优先使用StringBuilder类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。为了获得更好的性能,在构造StringBuffer或StringBuilder时应尽量指定她的容量。当然如果不超过16个字符时就不用了。 相同情况下,使用StringBuilder比使用StringBuffer仅能获得10%~15%的性能提升,但却要冒多线程不安全的风险。综合考虑还是建议使用StringBuffer。
对于常量字符串,用'String' 代替 'StringBuffer' 常量字符串并不需要动态改变长度。对于字符串拼接append方法最快、concat次之、加号最慢 大多数情况,我们使用“+”,符合编码习惯和我们的阅读 当在频繁进行字符串的运算(如拼接、替换、删除等),或者在系统性能临界的时候,我们可以考虑使用concat或append方法
12. 尽量减小同步作用范围, 应尽量使用方法同步代替代码块同步 synchronized 方法 vs. 代码块
13. 用ThreadLocal 缓存线程不安全的对象,SimpleDateFormat
public class SimpleDateFormUtils {
private static ThreadLocal<SimpleDateFormat> d=new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args){
d.get().format(new Date());
}
}
public class SimpleDateFormUtils {
private static final String MESSAGE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final ThreadLocal<DateFormat> messageFormat = new ThreadLocal< >();
private static final DateFormat getDateFormat() {
DateFormat format = messageFormat.get();
if (format == null) {
format = new SimpleDateFormat(MESSAGE_FORMAT, Locale.getDefault());
messageFormat.set(format);
}
return format;
}
public static void main(String[] args){
System.err.println(SimpleDateFormUtils.getDateFormat().format(new Date()));
}
}
14. 尽量使用延迟加载( 懒汉加载或者饿汉加载 )
String str = "aaa";
if (i == 1){
list.add(str);
}
建议改成:
if (i == 1){
String str = "aaa";
list.add(str);
}
15. 尽量减少使用反射,必须用时加缓存,用反射赋值对象,主要优点是节省了代码量,主要缺点却是性能有所下降。
List<UserDO> userDOList = ...; List<UserVO> userVOList = new ArrayList<>(userDOList.size()); for (UserDO userDO : userDOList) { UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); userVOList.add(userVO); } 正例: List<UserDO> userDOList = ...; List<UserVO> userVOList = new ArrayList<>(userDOList.size()); for (UserDO userDO : userDOList) { UserVO userVO = new UserVO(); userVO.setId(userDO.getId()); ... userVOList.add(userVO); }
16. 尽量使用连接池、线程池、对象池、缓存
17. 及时释放资源, I/O 流、Socket、数据库连接
18. 慎用异常,不要用抛异常来表示正常的业务逻辑 目前业务都手动抛异常处理 应该是个伪定论
异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程
19. String 操作尽量少用正则表达式
replace VS replaceAll
split
除非是必须的,否则应该避免使用split,split由于支持正则表达式,所以效率比较低,如果是频繁的几十,几百万的调用将会耗费大量资源,如果确实需要频繁的调用split,可以考虑使用apache的StringUtils.split(string,char),频繁split的可以缓存结果。
20. 日志输出注意使用不同的级别
21. 日志中参数拼接使用占位符
log.info("orderId:" + orderId); 不推荐, 会增加无畏的字符串拼接
log.info("orderId:{}", orderId); 推荐
log.debug( "Load student(name: {}" ,student.getName() );不推荐,student可能为null空指针
log.debug( "Load student(student: {}" , student );推荐
//判断当前配置下debug级别是否可以输出, 等同于下面的先判断,后打印避免无用的字符串拼接
logger.debug("入参报文:{}",() -> JSON.toJSONString(policyDTO));
if(logger.isDebugEnabled()){
logger.debug("入参报文:{}",JSON.toJSONString(policyDTO));
}
最牛逼 Java 日志框架 — Log4j2,性能无敌,横扫对手.....
22. 把一个基本数据类型转为字符串,包装数据类型(eg Integer).toString()是最快的方式、String.valueOf(数据)次之、数据+""最慢
for (int i = 0; i < A.length; i++) {
//int tmp=A[i];
//ints[i]=tmp*tmp;
ints[i]=A[i]*A[i]; //这种效率高
}
23. 尽量在合适的场合使用单例
使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:
-
控制资源的使用,通过线程同步来控制资源的并发访问;
-
控制实例的产生,以达到节约资源的目的;
-
控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
24. 尽量避免随意使用静态变量
当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,此时静态变量b的生命周期与A类同步,如果A类不会卸载,那么b对象会常驻内存,直到程序终止。
有的同学习惯使用对象访问静态变量,这种方式多了一步寻址操作,需要先找到变量对应的类,再找到类对应的变量,如下面的代码:
public class A{
private static B b = new B();
}
24(2) 使用静态代码块实现赋值静态成员变量
反例:
private static Map<String, Integer> map = new HashMap<String, Integer>(){ { map.put("Leo",1); map.put("Family-loving",2); map.put("Cold on the out side passionate on the inside",3); } };
正例:
private static Map<String, Integer> map = newHashMap<String, Integer>();
static{ map.put("Leo",1);
map.put("Family-loving",2);
map.put("Cold on the out side passionate on the inside",3);
}
25.尽量避免过多过常地创建Java对象
尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组来替代对象。
Long i = new Long(1L); String s = new String("abc"); 改成: Long i = 1L; String s = "abc";
26.尽量使用局部变量
在函数内,基本类型的参数和临时变量都保存在栈(Stack)中,访问速度较快;对象类型的参数和临时变量的引用都保存在栈(Stack)中,内容都保存在堆(Heap)中,访问速度较慢。在类中,任何类型的成员变量都保存在堆(Heap)中,访问速度较慢。
27. 尽量处理好包装类型和基本类型两者的使用场所
虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产生的内存区域是完全不同的,基本类型数据产生和处理都在栈中处理,包装类型是对象,是在堆中产生实例。在集合类对象,有对象方面需要的处理适用包装类型,其他的处理提倡使用基本类型。
28. 尽量使用移位来代替'a/b' 'a*b'的操作
"/" 和 '*' 是一个代价很高的操作,使用移位的操作将会更快和更有效 但注意的是使用移位应添加注释,因为移位操作不直观,比较难理解。
如:
int num = a / 4;
int num = a / 8;
应改为:
int num = a >> 2;
int num = a >> 3;
int num = a * 4;
int num = a * 8;
应改为:
int num = a << 2;
int num = a << 3;
29. 尽量避免使用二维数组
二维数据占用的内存空间比一维数组多得多,大概10倍以上。
30. 尽量缓存经常使用的对象
尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache进行缓存,他们基本都实现了FIFO/FLU等缓存算法。
31. 在java+Oracle的应用系统开发中,java中内嵌的SQL语言应尽量使用大写形式,以减少Oracle解析器的解析负担。
32. 在java编程过程中,进行数据库连接,I/O流操作,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销。
33. 不用new关键字创建对象的实例
用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。
下面是Factory模式的一个典型实现:
public static Credit getNewCredit(){
return new Credit();
}
改进后的代码使用clone()方法:
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit(){
return (Credit)BaseCredit.clone();
}
成员变量: 存在默认值(系统给定)
局部变量: 不存在默认值,必须先赋值,然后在使用
34:如果变量的初值会被覆盖,就没有必要给变量赋初值
List<UserDO> userList = new ArrayList<>(); if (isAll) { userList = userDAO.queryAll(); } else { userList = userDAO.queryActive(); } 改成: List<UserDO> userList; if (isAll) { userList = userDAO.queryAll(); } else { userList = userDAO.queryActive(); }
35. 对于多常量选择分支,尽量使用switch语句而不是if-else语句
if-else语句,每个if条件语句都要加装计算,直到if条件语句为true为止。switch语句进行了跳转优化,Java中采用tableswitch或lookupswitch指令实现,对于多常量选择分支处理效率更高。经过试验证明:在每个分支出现概率相同的情况下,低于5个分支时if-else语句效率更高,高于5个分支时switch语句效率更高。
反例:
if (i == 1) {
...; // 分支1
} else if (i == 2) {
...; // 分支2
} else if (i == ...) {
...; // 分支n
} else {
...; // 分支n+1
}
正例:switch (i) {
case 1 :
... // 分支1
break;
case 2 :
... // 分支2
break;
case ... :
... // 分支n
break;
default :
... // 分支n+1
break;
}
备注:如果业务复杂,可以采用Map实现策略模式。
36. MyBatis 不要为了多个查询条件而写 1 = 1
37.避免使用BigDecimal(double) , 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
反例:
BigDecimal bigDecimal = newBigDecimal(0.11D);
正例:
BigDecimal bigDecimal1 = BigDecimal.valueOf(0.11D);
38. 不要在多线程下使用同一个 Random
Random 类的 seed 会在并发访问的情况下发生竞争,造成性能降低,建议在多线程环境下使用 ThreadLocalRandom 类。 文章: 还在用 Random生成随机数了?试试 ThreadLocalRandom,好用!
使用: ThreadLocalRandom.current().nextInt(0, 100); 生成0-100随机数
在 Linux 上,通过加入 JVM 配置 -Djava.security.egd=file:/dev/./urandom,使用 urandom 随机生成器,在进行随机数获取时,速度会更快。
cat /dev/urandom | LC_ALL=C tr -dc "[:alnum:]" | fold -w 16 |head -3
说明: fold -w 16 指定密码的位数。 head -3 产成多少个密码。
生成特别复杂的密码: cat /dev/urandom | LC_ALL=C tr -dc "[:graph:]" | fold -w 16 |head -3
39. 自增推荐使用 LongAddr
自增运算可以通过 synchronized 和 volatile 的组合,或者也可以使用原子类(比如 AtomicLong)。 后者的速度比前者要高一些,AtomicLong 使用 CAS 进行比较替换,在线程多的情况下会造成过多无效自旋,所以可以使用 LongAdder 替换 AtomicLong 进行进一步的性能提升