1. JVM字节码

  • 前面我们通过tomcat本身的参数以及jvm的参数对tomcat做了优化,其实要想将应用程序跑的更快、效率更高,除了对tomcat容器以及jvm优化外,应用程序代码本身如果写的效率不高的,那么也是不行的,所以,对于程序本身的优化也就很重要了。
  • 对于程序本身的优化,可以借鉴很多前辈们的经验,但是有些时候,在从源码角度方面分析的话,不好鉴别出哪个效率高,如对字符串拼接的操作,是直接“+”号拼接效率高还是使用StringBuilder效率高?
  • 这个时候,就需要通过查看编译好的class文件中字节码,就可以找到答案。
  • 我们都知道,java编写应用,需要先通过javac命令编译成class文件,再通过jvm执行,
  • jvm执行时是需要将class文件中的字节码载入到jvm进行运行的。

通过javap命令查看class文件的字节码内容

首先,看一个简单的Test1类的代码:

public class Test1 {
    public static void main(String[] args) {
        int a = 2;
        int b = 5;
        int c = b - a;
        System.out.println(c);
    }
}

通过javap命令查看class文件中的字节码内容:

javap ‐v Test1.class > Test1.txt
javap用法: javap
其中, 可能的选项包括:
‐help ‐‐help ‐? 输出此用法消息
‐version 版本信息
‐v ‐verbose 输出附加信息
‐l 输出行号和本地变量表
‐public 仅显示公共类和成员
‐protected 显示受保护的/公共类和成员
‐package 显示程序包/受保护的/公共类
和成员 (默认)
‐p ‐private 显示所有类和成员
‐c 对代码进行反汇编
‐s 输出内部类型签名
‐sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
‐constants 显示最终常量
‐classpath <path> 指定查找用户类文件的位置
‐cp <path> 指定查找用户类文件的位置
‐bootclasspath <path> 覆盖引导类文件的位置 </path> </path> </path>

常量池

官网文档:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140

描述符

字段描述符
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

* 重点注意LClassName代表一个类的实例

方法描述符
官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3

示例:

代码执行的过程如下:


  • LineNumberTable: 将代码的行和字节码的行对应起来

图解

研究 i++ 与 ++i 的不同

我们都知道,i++表示,先返回再+1,++i表示,先+1再返回。它的底层是怎么样的呢? 我们一起探究下。

测试代码:

public class Test2 {
    public static void main(String[] args) {
        new Test2().method1();
        new Test2().method2();
    }
    public void method1(){
        int i = 1;
        int a = i++;
        System.out.println(a); //打印1
    }
    public void method2(){
        int i = 1;
        int a = ++i;
        System.out.println(a);//打印2
    }
}

对比

i++

++i

区别:

  • i++
    只是在本地变量中对数字做了相加,并没有将数据压入到操作栈
    将前面拿到的数字1,再次从操作栈中拿到,压入到本地变量中

  • ++i
    将本地变量中的数字做了相加,并且将数据压入到操作栈
    将操作栈中的数据,再次压入到本地变量中
    小结:可以通过查看字节码的方式对代码的底层做研究,探究其原理。

字符串拼接

public class Test3 {
    public static void main(String[] args) {
        new Test4().m1();
        new Test4().m2();
    } 
    public void m1(){
        String str = "";
        for (int i = 0; i < 5; i++) {
            str = str + i;
        } 
        System.out.println(str);
    } 
    public void m2(){
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            sb.append(i);
        } 
        
        System.out.println(sb.toString());
    }
}

m1() 与 m2() 哪个方法的效率高?


  • 可以看到,m1()方法中的循环体内,每一次循环都会创建StringBuilder对象,效率低于m2()方法

小结

使用字节码的方式可以很好查看代码底层的执行,从而可以看出哪些实现效率高,哪些实现效率低。可以更好的对我们的代码做优化。让程序执行效率更高.

2. 代码优化

优化,不仅仅是在运行环境进行优化,还需要在代码本身做优化,如果代码本身存在性能问题,那么在其他方面再怎么优化也不可能达到效果最优的

尽可能使用局部变量

  • 调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

尽量减少对变量的重复计算

  • 明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下面的操作:
for (int i = 0; i < list.size(); i++)
{...}
  • 建议替换为:
int length = list.size();
for (int i = 0, i < length; i++)
{...}
  • 这样,在list.size()很大的时候,就减少了很多的消耗。

尽量采用懒加载的策略,即在需要的时候才创建

String str = "aaa";
if (i == 1){
list.add(str);
}
//建议替换成
if (i == 1){
String str = "aaa";
list.add(str);
}

异常不应该用来控制程序流程

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

不要将数组声明为public static final

  • 因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。

不要创建一些不使用的对象,不要导入一些不使用的类

  • 这毫无意义,如果代码中出现"The value of the local variable i is not used"、“The import java.util is never used”,那么请删除这些无用的内容

程序运行过程中避免使用反射

  • 反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是 Method的invoke方法。
  • 如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存。
  • 启动的过程中,可以启动慢一点,用户感知不到,但是运行中,用户能感受到性能差

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

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

使用Entry遍历Map

Map<String,String> map = new HashMap<>();
for (Map.Entry<String,String> entry : map.entrySet()) {
    String key = entry.getKey();
    String value = entry.getValue();
}

避免使用这种方式

Map<String,String> map = new HashMap<>();
for (String key : map.keySet()) {
	String value = map.get(key);
}

不要手动调用System.gc();

  • 会影响JVM对gc的判断
  • 一遍在jvm中都会禁用显式GC

String尽量少用正则表达式

  • 正则表达式虽然功能强大,但是其效率较低,除非是有需要,否则尽可能少用。
  • replace() 不支持正则
  • replaceAll() 支持正则
  • 如果仅仅是字符的替换建议使用replace()。

日志的输出要注意级别

// 当前的日志级别是error
LOGGER.info("保存出错!" + user);
  • 考虑程序执行是否具有意义

对资源的close()建议分开操作

try{
XXX.close();
YYY.close();
} 
catch (Exception e){
...
}
 // 建议改为
try{
XXX.close();
}
 catch (Exception e){
...
} 
try{
YYY.close();
} 
catch (Exception e){
...
}
  • 防止出现相互的影响