飞道的博客

JVM性能优化(五)源码图解jvm字节码

476人阅读  评论(0)

一、前言

前面我们通过tomcat本身的参数以及jvm的参数对tomcat做了优化,详情查看:tomcat优化,其实要想将应用程序跑的更快,效率更高,除了对tomcat容器以及jvm优化外,应用程序代码本身如果写的效率不高的,那么也是不行的,所以对于程序本身的优化也就很重要了。

对于程序本身的优化,可以借鉴很多前辈的经验,但是有些时候,在从源码角度分析的话,不好鉴别出哪个效率高,如对字符串拼接的操作,是直接“+”号拼接效率高还是使用StringBuilder效率高呢?

这个时候,就需要通过查看编译好的class文件中的字节码,就可以找到答案。

我们都知道,java编写应用,需要先通过javac命令编译成class文件,在通过jvm执行,jvm执行时是需要将class文件中的字节码载入到jvm进行运行的

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

首先,看一下简单的Test类的代码:

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

通过javap命令查看class文件中的字节码内容:
javap -v Test.class > Test.txt

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

当我们运行javap命令后会得到一个Test.txt的文件
内容如下:

#  显示生成这个class的java源文件、版本信息、生成时间等
Classfile /F:/project/test/target/classes/com/lyy/Test.class
  Last modified 2020-5-7; size 562 bytes
  MD5 checksum 58100edcdfebfd9769cdbb1b634baf3c
  Compiled from "Test.java"
public class com.lyy.Test
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
 # 显示了该类中所涉及的常量池,共35个常量
Constant pool:
   #1 = Class              #2             // com/lyy/Test
   #2 = Utf8               com/lyy/Test
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/lyy/Test;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Fieldref           #17.#19        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Class              #18            // java/lang/System
  #18 = Utf8               java/lang/System
  #19 = NameAndType        #20:#21        // out:Ljava/io/PrintStream;
  #20 = Utf8               out
  #21 = Utf8               Ljava/io/PrintStream;
  #22 = Methodref          #23.#25        // java/io/PrintStream.println:(I)V
  #23 = Class              #24            // java/io/PrintStream
  #24 = Utf8               java/io/PrintStream
  #25 = NameAndType        #26:#27        // println:(I)V
  #26 = Utf8               println
  #27 = Utf8               (I)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               a
  #31 = Utf8               I
  #32 = Utf8               b
  #33 = Utf8               c
  #34 = Utf8               SourceFile
  #35 = Utf8               Test.java
 #显示该类的构造器,编译器自动插入的
{
  public com.lyy.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lyy/Test;
            
# 显示了main方的信息(这个是我们需要重点关注的)
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_2
         1: istore_1
         2: iconst_5
         3: istore_2
         4: iload_2
         5: iload_1
         6: isub
         7: istore_3
         8: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_3
        12: invokevirtual #22                 // Method java/io/PrintStream.println:(I)V
        15: return
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 4
        line 9: 8
        line 10: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            2      14     1     a   I
            4      12     2     b   I
            8       8     3     c   I
}
SourceFile: "Test.java"

生成的内容虽然看上去很多,但是总体来说,大致分为4个部分:

第一部分: 显示生成这个class的java源文件、版本信息、生成时间等
第二部分: 显示了该类中所涉及的常量池,共35个常量
第三部分: 显示该类的构造器,编译器自动插入的
第四部分: 显示了main方的信息(这个是我们需要重点关注的)

这么看的话我们是很难看懂里面的内容说的是什么的,我们需要对里面的参数一一作出说明,请往下看。

三、常量池

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

Constant Type Value 说明
CONSTANT_Class 7 类或接口的符号引用
CONSTANT_Fieldref 9 字段的符号引用
CONSTANT_Methodref 10 类中方法的符号引用
CONSTANT_InterfaceMethodref 11 接口中方法的符号引用
CONSTANT_String 8 字符串类型常量
CONSTANT_Integer 3 整形常量
CONSTANT_Float 4 浮点型常量
CONSTANT_Long 5 长整型常量
CONSTANT_Double 6 双精度浮点型常量
CONSTANT_NameAndType 12 字段或方法的符号引用
CONSTANT_Utf8 1 UTF-8编码的字符串
CONSTANT_MethodHandle 15 表示方法句柄
CONSTANT_MethodType 16 标志方法类型
CONSTANT_InvokeDynamic 18 表示一个动态方法调用点

四、描述符

4.1 字段描述符

api文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

术 语 类型 描述
B byte signed byte
C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L ClassName ; reference an instance of class ClassName
S short signed short
Z boolean true or false
[ reference one array dimension

4.1 方法描述符

示例:

方法的方法描述符:

4.2 解读方法字节码

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V	//方法描述,V表示该方法的返回值为void
    flags: ACC_PUBLIC, ACC_STATIC	//方法修饰符,public、static的
    Code:
	//stack=2 操作栈的大小为0
	//locals=4 本地变量表大小
	//args_size=1 参数的个数
      stack=2, locals=4, args_size=1
         0: iconst_2	//将数字2值压入操作栈,位于栈的最上面
         1: istore_1	//从操作栈中弹出一个元素(数字2),放入到本地变量表中,位于下标为1的位置(下标为0的是this)
         2: iconst_5	//将数字5值压入操作栈,位于栈的最上面
         3: istore_2	//从操作栈中弹出一个元素(5),放入本地变量表中,位于第下标为2个位置
         4: iload_2	//将本地变量表中下标为2的位置压入操作栈(5)
         5: iload_1	//将本地变量表中下标为1的位置压入操作栈(2)
         6: isub	//操作栈中的2个数字相减
         7: istore_3	//将相减的结果压入到本地变量表中,位于下标为3的位置
	//通过#16号找到对应的常量,即可找到对应的引用
         8: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_3	//将本地变量表中下标为3的位置元素压入操作栈(3)
	// 通过#22找到对应的常量,即可找到对应的引用,进行方法调用
        12: invokevirtual #22                 // Method java/io/PrintStream.println:(I)V
        15: return	//返回
      LineNumberTable:	//行号的列表
        line 6: 0
        line 7: 2
        line 8: 4
        line 9: 8
        line 10: 15
      LocalVariableTable:	//本地变量表
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            2      14     1     a   I
            4      12     2     b   I
            8       8     3     c   I
}
SourceFile: "Test.java"

4.3 图解方法字节码:

int a = 2:

int b = 5:

int c = b - a:

五、i++ 与 i++ 的不同

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

测试代码:

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);
		}
		
		public void method2(){
			int i = 1;
			int a = ++i;
			System.out.println(a);
		}

5.1 查看 class字节码

通过 命令 javap -v Test2.class > Test2.txt,查看class字节码

  public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: iinc          1, 1
         6: istore_2
         7: getstatic     #25                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #31                 // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 11: 0
        line 12: 2
        line 13: 7
        line 14: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/lyy/Test2;
            2      13     1     i   I
            7       8     2     a   I

  public void method2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iinc          1, 1
         5: iload_1
         6: istore_2
         7: getstatic     #25                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #31                 // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 17: 0
        line 18: 2
        line 19: 7
        line 20: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/lyy/Test2;
            2      13     1     i   I
            7       8     2     a   I
}
SourceFile: "Test2.java"

5.2 对比

5.2.1 i++
0: iconst_1	//将数字1压入操作栈
1: istore_1	//将数字1从操作栈弹出,压入到本地变量表中,下标为1
2: iload_1	//从本地变量表中获取下标为1的数据,压入到操作栈中
3: iinc    1, 1	//将本地变量中的1,再+1	
6: istore_2	//将数字1从操作栈中弹出,压入到本地变量表中,下标为2
7: getstatic     #25                 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_2	//从本地变量表中获取下标为2的数据,压入到操作栈中
11: invokevirtual #31                 // Method java/io/PrintStream.println:(I)V
14: return

图解:



i++打印结果:1

5.2.2 ++i
0: iconst_1	//将数字1压入到操作栈
1: istore_1	//将数字1从操作栈弹出,压入到本地变量表中,下标为1
2: iinc          1, 1//将本地变量中的1,再+1
5: iload_1	//从本地变量表中获取下标为1的数据(2),压入到操作栈中
6: istore_2	//将数字2从操作栈弹出,压入到本地变量表中,下标为2
7: getstatic     #25                 // Field 
10: iload_2	//从本地变量表中获取下标为2的数据(2),压入到操作栈中
11: invokevirtual #31                 // Method 
14: return

图解:


5.3 区别

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

六、代码优化

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

6.1 尽可能使用局部变量

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

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

for(int i = 0; i< list.size;i++)
{...}

#建议替换为:

int length = list.size();
for(int i = 0; i< length;i++)
{...}

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

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

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

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

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

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

6.6 ArrayList 随机遍历快,LinkedList添加删除快

七、小结

使用字节码的方式可以很好的查看代码底层的执行,从而可以看出那些实现效率高,那些实现效率低,可以更好的对我们代码做优化,让程序执行效率更高,今天的内容就到这里了,感兴趣的小伙伴记得关注我,大家加油~


转载:https://blog.csdn.net/qq_14996421/article/details/105966857
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场