一小篇文章教你看懂Java字节码

一小篇文章教你看懂Java字节码,第1张

1.认识JVM内存模型
public class Test {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

Java HelloWord 代码到底是如何执行的呢,要了解代码的执行过程,必须要了解一下JVM的内存模型,这里,我说一下我对JVM的理解,先上一张好图

​ JVM是这样定义的,虚拟机(VM: Virtual Machine)是通过软件模拟物理机器执行程序的执行器。

​ 这里,我们其实我们可以把JVM的执行过程类比成你想要制作某样东西的过程,例如玩具车,首先,得有原料,然后进行加工,而加工过程中,你得按步骤来是吧,每一步要做什么,用到什么,你得心理清楚吧,这样你才能一步步的进行,加工完成你最初想要的东西。这里,Class文件看做是生产原料,放在方法区,这里是最初的开始,即所有线程共享的地方,然后中的东西可以看做是单独加工好的一个个成型的零件,待组装的零件,这里也是所有线程共享的,而线程可以看做我们一次次制作的过程,因为每次我们加工是互不影响的,所以线程所使用到的内存数据都是相互独立的,一切准备就绪,好了你准备加工了,可是你怎么知道原料()具体有什么呢,所以有了一个清单,即常量池,运行的时候就会变成运行时常量池,这里列出了原料()的详细信息,以便让你方便查询使用,而程序计数器其实是记录了我们的加工步骤,如果你加工的时候突然有事,你可以先去办别的事,然后再回来继续,可以通过程序计数器,就可以定位到你上次执行到哪一步,看起来很妙不是吗。这里虚拟机栈就是核心步骤了,即开始进行制作,在虚拟机栈 *** 作这一步,极其关键,这里会来回的 *** 作数据,即入栈出栈,对,搞半天代码执行就是入栈出栈 *** 作,这里我们有一块区域叫做局部变量表,在虚拟机栈这里当做临时存储区域,在进行虚拟机栈入栈和出栈的时候,会来回在局部变量表里取数据和存数据。

​ 另外,上图提到的执行引擎,这个其实是相当复杂的,执行引擎包含了解释器、即时编译器和垃圾回收器,解释器和编译器是为了跨平台使用的,因为代码最终执行是要经过 *** 作系统,然后又进行CPU指令调度,但是我们的 *** 作系统又有不同的版本,所有为了适配所有的 *** 作系统,用解释器来进行翻译,将java字节码翻译成不同 *** 作系统要执行的机器语言,进行CPU调度,这玩意就是个胖翻译啊!另外,垃圾回收器主要就是负责GC回收的,这里先有个大概了解吧,还有本地方法,是java直接调的 *** 作系统底层的方法。对JVM的内存模型有了初步认识后,下面我们来看看JVM是怎么去执行Java代码的。

2.读懂JVM字节码(助记符)

​ 一段Java代码写完后,我们需要编译才能进行运行的,而JVM是将我们的Java代码编译成了Class字节码文件,这个Class字节码只能JVM可以识别,也就是这样,通过安装JVM得以实现跨平台执行,那么JVM是怎么执行Java代码的呢,通过查阅资料,深知,JVM是通过读取字节码文件中的一个一个 *** 作指令,每个 *** 作指令对应着二进制位,JVM通过执行 *** 作指令就可以执行相关代码,而通过二进制位去看 *** 作指令是相当难理解的,为此,JDK给我们提供了一写可用的分析性工具,其中通过javap 这个命令我们可以对字节码反编译成我们好理解的代码,反编译过后,所有的 *** 作指令都变成了我们人类看得懂的助记符,通过它,我们可以直接对字节码进行间接性的认识,从而进一步了解我们的代码是怎么被执行的。

Javap的相关命令选项

-help 输出 javap 的帮助信息。
  -l 输出行及局部变量表。
  -b 确保与 JDK 1.1 javap 的向后兼容性。
  -public 只显示 public 类及成员。
  -protected 只显示 protectedpublic 类及成员。
  -package 只显示包、protectedpublic 类及成员。这是缺省设置。
  -private 显示所有类和成员。
  -J[flag] 直接将 flag 传给运行时系统。
  -s 输出内部类型签名。
  -c 输出类中各方法的未解析的代码,即构成 Java 字节码的指令。
  -verbose 输出堆栈大小、各方法的 locals 及 args 数,以及class文件的编译版本
  -classpath[路径] 指定 javap 用来查找类的路径。如果设置了该选项,则它将覆盖缺省值或 CLASSPATH 环境变量。目录用冒号分隔。
​	-bootclasspath[路径] 指定加载自举类所用的路径。缺省情况下,自举类是实现核心 Java 平台的类,位于 jrelib下面。
  -extdirs[dirs] 覆盖搜索安装方式扩展的位置。扩展的缺省位置是 jrelibext。

HelloWord的字节码是如何执行的呢?

下面是使用javap -v 反编译HelloWord程序的字节码,我都标注了每一处具体是干啥的

  Last modified 2022-5-4; size 544 bytes //修改时间,所需要的的字节大小
 MD5 checksum e603dd8515c39e94ee7169795b7cab14 //MD5 检验码
  Compiled from "Test.java" //编译的源程序文件
public class com.xlape.jvm.Test //类
  minor version: 0
  major version: 52//这两处是代表着jdk的版本,50是JDK1.6,51是jDK1.7,52是JDK1.8
  flags: ACC_PUBLIC, ACC_SUPER //访问修饰符
Constant pool://编译期常量池,运行时会指向具体的索引地址,类的相关信息会存储在这里,后面指令调用的时候都会来常量池中取对应的东西
   #1 = Methodref          #6.#20         // java/lang/Object."":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World!
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/xlape/jvm/Test
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/xlape/jvm/Test;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Test.java
  #20 = NameAndType        #7:#8          // "":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World!
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/xlape/jvm/Test
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com.xlape.jvm.Test();//默认的构造方法
    descriptor: ()V //无参构造
    flags: ACC_PUBLIC //访问修饰符为PUBLIC
    Code://要执行的代码片段
      stack=1, locals=1, args_size=1 //栈的深度|本地局部变量表长度|参数个数
         0: aload_0 //将本地局部变量表的第0号位置加载到 *** 作数组栈,0号位置是指this
         1: invokespecial #1 //调用默认构造函数方法                  // Method java/lang/Object."":()V
         4: return //返回
      LineNumberTable: //行号表,记录源代码和字节码对应的行号
        line 3: 0 //源代码行号|字节码行号
      LocalVariableTable: //局部变量表
        Start  Length  Slot  Name   Signature //字节码的偏移量起始位置|字节码的偏移量结束位置|存储的基本单位,槽位,索引位|变量名|方法签名
            0       5     0  this   Lcom/xlape/jvm/Test;

  public static void main(java.lang.String[]); //main方法
    descriptor: ([Ljava/lang/String;)V //参数描述,有一个String的参数变量
    flags: ACC_PUBLIC, ACC_STATIC //访问修饰符
    Code: //代码片段
      stack=2, locals=1, args_size=1 //栈的深度|本地局部变量表长度|参数个数
         0: getstatic     #2  //去常量池中找2号位置,如果还有引用,依次往下找  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3 //将3号位置Hello World入 *** 作数组栈                // String Hello World!
         5: invokevirtual #4 //调用 4号位置中的方法输出             // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return //放回
      LineNumberTable:
        line 5: 0
        line 6: 8 //源代码行号|字节码行号
      LocalVariableTable: //局部变量表
        Start  Length  Slot  Name   Signature //字节码的偏移量起始位置|字节码的偏移量结束位置|存储的基本单位,槽位,索引位|变量名|方法签名
            0       9     0  args   [Ljava/lang/String;
}

​ 通过对HelloWorld程序的反编译分析知道,如果进行方法调用,则会按照相应的代码 *** 作指令去一步步执行,在指令调用的时候,会从常量池中拿取相关信息,压人 *** 作数栈,并且执行相关的方法,在给局部变量赋值的时候,会调用指令将数据存入局部变量表中,相反在使用局部量的时候,会从局部变量表中拿去数压入 *** 作数栈,进行相关方法的调用以及返回。

​ 也通过以上了解了java字节码的执行细节,当然JVM底层要做的远远不止这些,并且以上只是对Hellword这个程序进行了反编译,实际还有更复杂的类和方法体,会出现更多的 *** 作指令,这里摘抄了一篇好文中对于JVM *** 作指令的总结:便于后面查阅和使用

栈 *** 作相关
  • load 命令:用于将局部变量表的指定位置的相应类型变量加载到栈顶;

  • store命令:用于将栈顶的相应类型数据保入局部变量表的指定位置;

变量进栈含义变量保存含义
iload第1个int型变量进栈istore栈顶int数值存入第1局部变量
iload_0第1个int型变量进栈istore_0栈顶int数值存入第1局部变量
iload_1第2个int型变量进栈istore_1栈顶int数值存入第2局部变量
iload_2第3个int型变量进栈istore_2栈顶int数值存入第3局部变量
iload_3第4个int型变量进栈istore_3栈顶int数值存入第4局部变量
lload第1个long型变量进栈lstore栈顶long数值存入第1局部变量
fload第1个float型变量进栈fstore栈顶float数值存入第1局部变量
dload第1个double型变量进栈dstore栈顶double数值存入第1局部变量
aload第1个ref型变量进栈astore栈顶ref对象存入第1局部变量

const、push和ldc

  • const、push:将相应类型的常量放入栈顶
  • ldc:则是从常量池中将常量
常量进栈含义
aconst_nullnull进栈
iconst_m1int型常量-1进栈
iconst_0int型常量0进栈
iconst_1int型常量1进栈
iconst_2int型常量2进栈
iconst_3int型常量3进栈
iconst_4int型常量4进栈
iconst_5int型常量5进栈
lconst_0long型常量0进栈
fconst_0float型常量0进栈
dconst_0double型常量0进栈
bipushbyte型常量进栈
sipushshort型常量进栈
常量池 *** 作含义
ldcint、float或String型常量从常量池推送至栈顶
ldc_wint、float或String型常量从常量池推送至栈顶(宽索引)
ldc2_wlong或double型常量从常量池推送至栈顶(宽索引)
pop和dup
  • pop用于栈顶数值出栈 *** 作;
  • dup用于赋值栈顶的指定个数的数值,并将其压入栈顶指定次数;
栈顶 *** 作含义
pop栈顶数值出栈(不能是long/double)
pop2栈顶数值出栈(long/double型1个,其他2个)
dup复制栈顶数值,并压入栈顶
dup_x1复制栈顶数值,并压入栈顶2次
dup_x2复制栈顶数值,并压入栈顶3次
dup2复制栈顶2个数值,并压入栈顶
dup2_x1复制栈顶2个数值,并压入栈顶2次
dup2_x2复制栈顶2个数值,并压入栈顶3次
swap栈顶的两个数值互换,且不能是long/double
对象相关
字段调用含义
getstatic获取类的静态字段,将其值压入栈顶
putstatic给类的静态字段赋值
getfield获取对象的字段,将其值压入栈顶
putfield给对象的字段赋值
方法调用作用解释
invokevirtual调用实例方法虚方法分派
invokestatic调用类方法static方法
invokeinterface调用接口方法运行时搜索合适方法调用
invokespecial调用特殊实例方法包括实例初始化方法、父类方法
invokedynamic由用户引导方法决定运行时动态解析出调用点限定符所引用方法
方法返回含义
ireturn当前方法返回int
lreturn当前方法返回long
freturn当前方法返回float
dreturn当前方法返回double
areturn当前方法返回ref
对象和数组
  • 创建类实例: new
  • 创建数组:newarray、anewarray、multianewarray
  • 数组元素 加载到 *** 作数栈:xaload (x可为b,c,s,i,l,f,d,a)
  • *** 作数栈的值 存储到数组元素: xastore (x可为b,c,s,i,l,f,d,a)
  • 数组长度:arraylength
  • 类实例类型:instanceof、checkcast
运算指令

运算指令是用于对 *** 作数栈上的两个数值进行某种运算,并把结果重新存入到 *** 作栈顶。Java虚拟机只支持整型和浮点型两类数据的运算指令,所有指令如下:

运算intlongfloatdouble
加法iaddladdfadddadd
减法isublsubfsubdsub
乘法imullmulfmuldmul
除法idivldivfdivddiv
求余iremlremfremdrem
取反ineglnegfnegdneg

其他运算:

  • 位移:ishl,ishr,iushr,lshl,lshr,lushr
  • 按位或: ior,lor
  • 按位与: iand, land
  • 按位异或: ixor, lxor
  • 自增:iin
  • 比较:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
类型转换

类型转换用于将两种不同类型的数值进行转换。

  • 对于宽化类型转换(小范围向大范围转换),无需显式的转换指令,并且是安全的 *** 作。各种范围从小到大依次排序: int, long, float, double。

  • 对于窄化类型转换,必须显式地调用类型转换指令,并且该过程很可能导致精度丢失。转换规则中需要特别注意的是当浮点值为NaN, 则转换结果为int或long的0。虽然窄化运算可能会发生上/下限溢出和精度丢失等情况,但虚拟机规范明确规定窄化转换U不可能导致虚拟机抛出异常。

  • 类型转换指令:i2b, i2c,f2i等等。

流程控制

控制指令是指有条件或无条件地修改PC寄存器的值,从而达到控制流程的目标

  • 条件分支:ifeq、iflt、ifnull、ifnonnull等
  • 复合分支:tableswitch、lookupswitch
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret
同步与异常

异常:

Java程序显式抛出异常: athrow指令。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现,而是采用异常表来完成。

同步:

方法级的同步和方法内部分代码的同步,都是依靠管程(Monitor)来实现的。

Java语言使用synchronized语句块,那么Java虚拟机的指令集中通过monitorenter和monitorexit两条指令来完成synchronized的功能。为了保证monitorenter和monitorexit指令一定能成对的调用(不管方法正常结束还是异常结束),编译器会自动生成一个异常处理器,该异常处理器的主要目的是用于执行monitorexit指令。


​ 以上只是包含我对JVM的一点认识,后面我会深入了解JVM更多的相关知识,但是我知道,通过本文学习至少能看懂JVM的字节码指令了,能通过Javap 的命令进行字节码分析,从而对java代码的执行原理有了进一步的理解,我觉得这是每一个Java程序员都应该具备的能力,如果有什么写的不对的,欢迎留言指出,大家共同进步!

革命尚未成功,猿某人还需努力啊!

参考资料:
图片来源

指令总结摘抄自

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/869590.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存