计算机只认识0和1。这意味着任何语言编写的程序最终都需要经过编译器编译成机器码才能被计算机执行。所以,我们所编写的程序在不同的平台上运行前都要经过重新编译才能被执行。 而Java刚诞生的时候曾经提过一个非常著名的宣传口号: "一次编写,到处运行" 。
Write Once, Run Anywhere.
为了实现该目的,Sun公司以及其他虚拟机提供商发布了许多可以运行在不同平台上的JVM虚拟机,而这些虚拟机都拥有一个共同的功能,那就是可以载入和执行同一种与平台无关的字节码(ByteCode)。 于是,我们的源代码不再必须根据不同平台翻译成0和1,而是间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。 如今,JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等。
源代码中的各种变量,关键字和运算符号的语义最终都会编译成多条字节码命令。而字节码命令所能提供的语义描述能力是要明显强于Java本身的,所以有其他一些同样基于JVM的语言能提供许多Java所不支持的语言特性。
例子
下面以一个简单的例子来逐步讲解字节码。
//Main.javapublic 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)
结果验证成立。
继续往下是常量池。但我并不打算继续直接分析这个十六进制文件。
反编译字节码文件
使用到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."<init>":()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 <init> #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 // "<init>":()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."<init>":()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 ,访问标志的含义如下:
ACC_PUBLIC | 0x0001 | 是否为Public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义. |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说, 次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
**ACC_ENUM ** | 0x4000 | 标志这是一个枚举 |
常量池
Constant pool
意为常量池。 常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:
-
类和接口的全限定名(Fully Qualified Name)
-
字段的名称和描述符号(Descriptor)
-
方法的名称和描述符
不同于C/C++, JVM是在加载Class文件的时候才进行的动态链接,也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中。
直接通过反编译文件来查看字节码内容:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V #4 = Class #21 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #18 = NameAndType #7:#8 // "<init>":()V #21 = Utf8 java/lang/Object
第一个常量是一个方法定义,指向了第4和第18个常量。以此类推查看第4和第18个常量。最后可以拼接成第一个常量右侧的注释内容:
java/lang/Object."<init>":()V
这段可以理解为该类的实例构造器的声明,由于Main类没有重写构造方法,所以调用的是父类的构造方法。此处也说明了Main类的直接父类是Object。 该方法默认返回值是V, 也就是void,无返回值。
同理可分析第二个常量:
#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
此处声明了一个字段m,类型为I, I即是int类型。关于字节码的类型对应如下:
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,以分号结尾,如Ljava/lang/Object; |
对于数组类型,每一位使用一个前置的"["字符来描述,如定义一个java.lang.String[][]类型的维数组,将被记录为"[[Ljava/lang/String;"
方法表集合
在常量池之后的是对类内部的方法描述,在字节码中以表的集合形式表现,暂且不管字节码文件的16进制文件内容如何,我们直接看反编译后的内容。
private int m; descriptor: I flags: ACC_PRIVATE
此处声明了一个私有变量m,类型为int,返回值为int
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."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/rhythm7/Main;
这里是构造方法:Main(),返回值为void, 公开方法。 code内的主要属性为:
-
stack最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1
-
locals:局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。
-
args_size:方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this
-
attribute_info方法体内容,0,1,4为字节码"行号",该段代码的意思是将第一个引用类型本地变量推送至栈顶,然后执行该类型的实例方法,也就是常量池存放的第一个变量,也就是注释里的"java/lang/Object."":()V", 然后执行返回语句,结束方法。
-
LineNumberTable该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用 -g:none 或-g:lines选项来取消或要求生成这项信息,如果选择不生成LineNumberTable,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码的行数来调试程序。
-
LocalVariableTable该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。 start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。
同理可以分析Main类中的另一个方法"inc()": 方法体内的内容是:将this入栈,获取字段#2, 将int类型的1入栈,将栈内顶部的两个数值相加,返回一个int类型的值。
SourceFile
源码文件名称
实战
分析try-catch-finally
通过以上一个最简单的例子,可以大致了解源码被编译成字节码后是什么样子的。 下面利用所学的知识点来分析一些Java问题:
public class TestCode { public int foo() { int x; try { x = 1; return x; } catch (Exception e) { x = 2; return x; } finally { x = 3; } } }
试问当不发生异常和发生异常的情况下,foo()的返回值分别是多少。 使出老手段
javac TestCode.java javap -verbose TestCode.class
查看字节码的foo方法内容:
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
在字节码的4,5,以及13,14中执行的是同一个操作,就是将int型的3入操作数栈顶,并存入第二个局部变量。这正是我们源码在finally语句块中内容。也就是说,JVM在处理异常时,会在每个可能的分支都将finally语句重复执行一遍。 通过一步步分析字节码,可以得出最后的运行结果是:
-
不发生异常时: return 1
-
发生异常时: return 2
-
发生非Exception及其子类的异常,抛出异常,不返回值
以上例子来自于《深入理解Java虚拟机 JVM高级特性与最佳实践》 关于虚拟机字节码指令表,也可以在《深入理解Java虚拟机 JVM高级特性与最佳实践-附录B》中获取。
kotlin 函数扩展的实现
kotlin提供了扩展函数的语言特性,借助这个特性,我们可以给任意对象添加自定义方法。 以下示例为Object添加"sayHello"方法
//SayHello.kt package com.rhythm7 fun Any.sayHello() { println("Hello") }
编译后,使用javap查看生成SayHelloKt.class文件的字节码。
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"
观察头部发现,koltin为文件SayHello生成了一个类,类名"com.rhythm7.SayHelloKt". 由于我们一开始编写SayHello.kt时并不希望SayHello是一个可实例化的对象类,所以,SayHelloKt是无法被实例化的,SayHelloKt并没有任何一个构造器。 再观察唯一的一个方法:发现 Any.sayHello()
的具体实现是静态不可变方法的形式:
public static final void sayHello(java.lang.Object);
所以当我们在其他地方使用 Any.sayHello()
时,事实上等同于调用java的 SayHelloKt.sayHello(Object)
方法。 顺便一提的是,当扩展的方法为Any时,意味着Any是non-null的,这时,编译器会在方法体的开头检查参数的非空,即调用 kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(Object value, String paramName)
方法来检查传入的Any类型对象是否为空。如果我们扩展的函数为 Any?.sayHello()
,那么在编译后的文件中则不会有这段字节码的出现。
相关推荐
标题《轻松看懂Java字节码.pdf》中隐藏的知识点是理解Java字节码的重要性及如何轻松掌握。描述中提到Java字节码是实现“一次编写,到处运行”(Write Once, Run Anywhere)这一Java承诺的核心技术之一。而标签“Java...
Java字节码加密是保护Java应用程序源代码安全的重要技术手段,主要是为了防止恶意用户逆向工程分析、篡改或盗取程序的核心逻辑。在Java中,字节码(Bytecode)是程序经过编译后的中间表示,可以直接由Java虚拟机...
Java 字节码概述 Java 字节码是 Java 虚拟机(JVM)执行 Java 语言编译后的结果。Java 字节码是一种平台无关的中间形式,能够在不同的操作系统和硬件平台上运行。Java 字节码的执行是由 JVM 负责的,它将字节码翻译...
Java字节码转换工具Retrotranslator是一个用于解决软件兼容性问题的实用工具,尤其是在Java版本升级带来的不兼容性上。随着Java技术的不断迭代,新版本的特性常常不能在旧版本的JDK环境下运行,而Retrotranslator的...
标题中的“class运行器v6”是一个用于执行Java字节码文件的应用程序,它允许用户在没有完整Java环境的情况下运行单个.class文件。这个工具可能是由开发者为了方便测试或教学目的而创建的,特别是对于那些不熟悉或者...
Java字节码编辑器是一种工具,它允许开发者直接编辑Java程序编译后的`.class`文件,而不是反编译后再重新编译。这种编辑器对于理解、调试和优化Java代码非常有用,尤其是对于那些无法访问源代码或者需要进行底层操作...
Java字节码文件查看工具,如JD-GUI,是开发者们深入理解Java应用程序内部机制的重要辅助工具。这类工具能够帮助我们查看并分析.class文件,这些文件是Java源代码经过编译后的二进制形式,包含了运行时所需的所有指令...
Java字节码分析工具,系统分析了java字节码文件,即java class类文件,对该文件中的各种成分以树的形式描述出来,只能针对未加密的class文件,一般由标准java编译器编译生成的class文件都未加密,该系统在vs2003下面...
Java 字节码编程 Java 字节码是 Java 程序的中间表示形式,它可以被 Java 虚拟机(JVM)解释执行。了解 Java 字节码可以帮助开发者更好地理解 Java 程序的执行机制,提高程序的执行效率和排除错误。 一、Java 类...
Java 字节码简单说明 Java 字节码是 Java 跨平台的基础,它使得 Java 程序可以在不同的平台上运行,而不需要重新编译。Java 字节码是平台无关性的基石,也是语言无关性的基础。 Class 文件结构是 Java 字节码的...
Java 字节码解读 Java 字节码是一种中间形式的代码,它是 Java 源代码编译后的结果。Java 字节码是平台独立的,可以在任何支持 Java 的设备上运行。Java 字节码的结构主要包括魔数、主版本号、次版本号、常量池、...
Java字节码是Java编程语言的一个重要特性,它在Java程序执行过程中扮演着核心角色。本文将深入探讨Java字节码的基本概念、作用以及其在Java虚拟机(JVM)中的运行机制。 Java字节码是一种低级的、平台无关的指令集...
Java字节码是Java程序在运行时被JVM(Java虚拟机)解释执行的一种中间语言。每个Java类都由一个`.class`文件表示,其中包含了编译后的字节码指令。`.class`文件的结构非常严谨,它不仅包含了类的信息,如类名、方法...
【Java字节码结构解析】 Java程序在执行时,首先需要通过Java编译器将源代码(.java文件)编译成二进制的字节码文件(.class文件),这些字节码由Java虚拟机(JVM)解析并执行。深入理解字节码结构有助于我们了解...
**JAVA字节码操作库 BCEL** BCEL(Byte Code Engineering Library)是Java开发的一个重要工具,主要用于处理Java字节码。它为开发者提供了一种深入理解与操作Java类文件的底层机制,允许分析、创建、修改和优化字节...
Java字节码实现AOP(面向切面编程)是一种在程序运行时动态插入代码的技术,它使得我们可以在不修改原有代码的情况下,增加新的功能或监控已有功能。在Java中,AOP通常通过代理模式和字节码操作来实现,如Spring AOP...
**JByteMod** 是一款专为Java开发者设计的字节码编辑器,它允许用户对JAR文件进行深度操作和修改。在Java编程环境中,字节码是Java虚拟机(JVM)执行的二进制代码,而JByteMod提供了一个用户友好的界面,使得开发者...
javassist, Java字节码工程工具包 Java字节码工程工具包 版本 3版权所有( C ) 1999 -2017按 Shigeru Chiba,保留所有权利。Javassist ( Java编程助手) 使Java字节码操作简单。 它是一个类库,用于在Java中编辑字节码
**Recaf:现代Java字节码编辑器** 在Java开发领域,字节码编辑器是一种不可或缺的工具,它允许开发者查看、分析并修改Java类文件的字节码,从而实现对程序运行时行为的深入控制。Recaf是这样一个现代、用户友好的...