轻松看懂Java字节码,看了都说好!
共 11688字,需浏览 24分钟
·
2020-10-19 16:13
/ Java字节码 /
计算机只认识0和1。这意味着任何语言编写的程序最终都需要经过编译器编译成机器码才能被计算机执行。所以,我们所编写的程序在不同的平台上运行前都要经过重新编译才能被执行。而Java刚诞生的时候曾经提过一个非常著名的宣传口号: "一次编写,到处运行"。
Write Once, Run Anywhere.
为了实现该目的,Sun公司以及其他虚拟机提供商发布了许多可以运行在不同平台上的JVM虚拟机,而这些虚拟机都拥有一个共同的功能,那就是可以载入和执行同一种与平台无关的字节码(ByteCode)。
于是,我们的源代码不再必须根据不同平台翻译成0和1,而是间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。
如今,JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等。
源代码中的各种变量,关键字和运算符号的语义最终都会编译成多条字节码命令。而字节码命令所能提供的语义描述能力是要明显强于Java本身的,所以有其他一些同样基于JVM的语言能提供许多Java所不支持的语言特性。
/ 例子 /
下面以一个简单的例子来逐步讲解字节码。
//Main.java
public class Main {
private int m;
public int inc() {
return m + 1;
}
}
通过以下命令, 可以在当前所在路径下生成一个 Main.class 文件。
javac Main.java
以文本的形式打开生成的class文件,内容如下:
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4d61 696e 2e6a 6176 610c
0007 0008 0c00 0500 0601 0010 636f 6d2f
7268 7974 686d 372f 4d61 696e 0100 106a
6176 612f 6c61 6e67 2f4f 626a 6563 7400
2100 0300 0400 0000 0100 0200 0500 0600
0000 0200 0100 0700 0800 0100 0900 0000
1d00 0100 0100 0000 052a b700 01b1 0000
0001 000a 0000 0006 0001 0000 0003 0001
000b 000c 0001 0009 0000 001f 0002 0001
0000 0007 2ab4 0002 0460 ac00 0000 0100
0a00 0000 0600 0100 0000 0800 0100 0d00
0000 0200 0e
对于文件中的16进制代码,除了开头的cafe babe,剩下的内容大致可以翻译成:啥玩意啊这......
英雄莫慌,我们就从我们所能认识的"cafe babe"讲起吧。文件开头的4个字节称之为 魔数,唯有以"cafe babe"开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。
目光右移,0000是编译器jdk版本的次版本号0,0034转化为十进制是52,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为1.8.0。通过java -version命令稍加验证, 可得结果。
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
结果验证成立。
继续往下是常量池。但我并不打算继续直接分析这个十六进制文件,这样会比较繁琐,我们通过另一种更容易让人看懂的方式来分析这个class文件。
反编译字节码文件
使用到java内置的一个反编译工具javap可以反编译字节码文件。通过javap -help可了解javap的基本用法
用法: 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 -verbose -p Main.class查看输出内容:
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
Last modified 2018-4-7; size 362 bytes
MD5 checksum 4aed8540b098992663b7ba08c65312de
Compiled from "Main.java"
public class com.rhythm7.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."":()V
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 = Class #20 // com/rhythm7/Main
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/rhythm7/Main;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Main.java
#18 = NameAndType #7:#8 // "":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
#21 = Utf8 java/lang/Object
{
private int m;
descriptor: I
flags: ACC_PRIVATE
public com.rhythm7.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
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/rhythm7/Main;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/rhythm7/Main;
}
SourceFile: "Main.java"
字节码文件信息
开头的7行信息包括:Class文件当前所在位置,最后修改时间,文件大小,MD5值,编译自哪个文件,类的全限定名,jdk次版本号,主版本号。然后紧接着的是该类的访问标志:ACC_PUBLIC, ACC_SUPER,访问标志的含义如下:
常量池
Constant pool意为常量池。常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:
类和接口的全限定名(Fully Qualified Name) 字段的名称和描述符号(Descriptor) 方法的名称和描述符
#1 = Methodref #4.#18 // java/lang/Object." ":()V
#4 = Class #21 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#18 = NameAndType #7:#8 // "":()V
#21 = Utf8 java/lang/Object
java/lang/Object." " :()V
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 = Class #20 // com/rhythm7/Main
#5 = Utf8 m
#6 = Utf8 I
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
方法表集合
private int m;
descriptor: I
flags: ACC_PRIVATE
public com.rhythm7.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
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/rhythm7/Main;
SourceFile
分析try-catch-finally
public class TestCode {
public int foo() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
public class TestCode {
javac TestCode.java
javap -verbose TestCode.class
public int foo();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 //int型1入栈 ->栈顶=1
1: istore_1 //将栈顶的int型数值存入第二个局部变量 ->局部2=1
2: iload_1 //将第二个int型局部变量推送至栈顶 ->栈顶=1
3: istore_2 //!!将栈顶int型数值存入第三个局部变量 ->局部3=1
4: iconst_3 //int型3入栈 ->栈顶=3
5: istore_1 //将栈顶的int型数值存入第二个局部变量 ->局部2=3
6: iload_2 //!!将第三个int型局部变量推送至栈顶 ->栈顶=1
7: ireturn //从当前方法返回栈顶int数值 ->1
8: astore_2 // ->局部3=Exception
9: iconst_2 // ->栈顶=2
10: istore_1 // ->局部2=2
11: iload_1 //->栈顶=2
12: istore_3 //!! ->局部4=2
13: iconst_3 // ->栈顶=3
14: istore_1 // ->局部1=3
15: iload_3 //!! ->栈顶=2
16: ireturn // -> 2
17: astore 4 //将栈顶引用型数值存入第五个局部变量=any
19: iconst_3 //将int型数值3入栈 -> 栈顶3
20: istore_1 //将栈顶第一个int数值存入第二个局部变量 -> 局部2=3
21: aload 4 //将局部第五个局部变量(引用型)推送至栈顶
23: athrow //将栈顶的异常抛出
Exception table:
from to target type
0 4 8 Class java/lang/Exception //0到4行对应的异常,对应#8中储存的异常
0 4 17 any //Exeption之外的其他异常
8 13 17 any
17 19 17 any
不发生异常时: return 1 发生异常时: return 2 发生非Exception及其子类的异常,抛出异常,不返回值
kotlin 函数扩展的实现
//SayHello.kt
package com.rhythm7
fun Any.sayHello() {
println("Hello")
}
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/SayHelloKt.class
Last modified 2018-4-8; size 958 bytes
MD5 checksum 780a04b75a91be7605cac4655b499f19
Compiled from "SayHello.kt"
public final class com.rhythm7.SayHelloKt
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
//省略常量池部分字节码
{
public static final void sayHello(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: ldc #9 // String $receiver
3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: ldc #17 // String Hello
8: astore_1
9: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
13: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
16: return
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 $receiver Ljava/lang/Object;
LineNumberTable:
line 4: 6
line 5: 16
RuntimeInvisibleParameterAnnotations:
0:
0: #7()
}
SourceFile: "SayHello.kt"
public static final void sayHello(java.lang.Object);
来源:juejin.im/post/6844903588716609543
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!