JVM 字节码

JVM 字节码

大家都知道 Java 是一门基于虚拟机的语言,我们只需要提供虚拟机能够执行的字节码就好了,每一个平台的不同实现的 JVM 虚拟机都执行同样的 JVM 字节码,这样也就实现了完美的跨平台,So cool。当然 JVM 字节码可不是 Java 语言所独有的,理论上任意的语言都可以编译成 JVM 字节码就可以供 JVM 虚拟机运行,只要你开发对应的编译器。 常见的 JVM 语言有,Kotlin、 Scala 和 Groovy 等等。

了解一些 JVM 字节码的一些基础知识还是有必要的,当出现某些 Bug 的时候,我们可以通过字节码的方式来定位一些通过 Java 源代码无法定位到的问题,因为字节码的颗粒度比源代码更加细;如果你要通过 ASM 来完成对字节码的插桩,来完成某项功能,字节码就是一门必修课;还可以通过阅读某些语言编译后的字节码来学习这门语言,比如 Kotlin。

文章开头我会先简单介绍一下 class 字节码文件的标准组成,这部分对阅读字节码指令的影响不大,读者可以按照自己的需求跳过,后面就重点介绍方法中重要的字节码指令,工具和例子分析。

1 JVM class 文件基本结构

1.1 基本结构

1.2 头部

4 Bytes:CAFE BABE (固定,也就是常说的 MAGIC NUM,咖啡宝贝,用来识别是否为class字节码文件)

2 Bytes: 次版本号 (绝大部分情况为0)

2 Bytes: 主要版本号 (高版本的 JVM 虚拟机可以运行低版本编译器编译的字节码,比如 1.5 的编译器编译的字节码,1.8 的虚拟机是可以运行的,反之则不行)

1.3 常量池

常量池中会用来存放很多的信息,包括类的定义,方法的定义,成员变量的定义,还有类中用到的8种基本类型和字符串等等等等,其他地方如果需要这些信息,通过编号获取。这也是字节码中占用空间的大头。

所有的类型:

不同的类型的数据结构:

1.4 类的基本信息

2 Bytes: 访问标记 (也就是 Public ,Protect,Private 啥的)

访问标记表(属性和方法也适用):

2 Bytes: 类索引

2 Bytes: 父类索引

2 Bytes: 实现接口数量

?Bytes:实现的接口信息

1.5 成员变量

2 Bytes: 成员变量数量

?Bytes:成员变量信息

成员变量数据结构:

1.6 方法

2 Bytes:方法数量

? Bytes: 方法信息

方法基本信息:

方法code部分基本信息:

方法方法 code 部分是很重要的部分,也是我们平时分析问题时最多的部分。其中包括重要的信息:最大操作数栈,本地变量表最大数,字节码长度,字节码操作指令,异常表,本地变量表,行号表 (也就是源码中的行号对应的字节码行号)

1.7 属性表

这个属性表是用来描述本 class 类中的一些熟悉的,不要和成员变量搞混了,比如说该 class 文件对应的源代码文件是什么,其中包含有哪些内部类。

2 Bytes: 属性表长度

?Bytes:属性信息

2 方法

2.1 方法栈帧

2.1.1 本地变量表

本地变量表储存了方法参数,当前对象引用(也就是this,如果不是静态方法的话),局部变量。本地变量表中的储存单位是叫做 Slot (通常是 4 个字节),除了 long 和 double 类型的变量占用 2 个 Slot,其他类型都占用一个 Slot。 当运行的指令需要的参数通常是从本地变量表中加载到操作数栈中,指令返回的数据如果需要供后面的操作使用,就需要再从操作数栈中保存到本地变量表中。

这里注意本地变量表的大小不是所有变量的个数加起来的,因为变量表的位置是可以重复被不同的变量使用的,当可以确定某个 Slot 的数据不会在后续的操作中使用时,就可以被其他的变量使用,避免重新分配 Slot,减少内存的浪费。在方法的信息里面有最大的 Slot 的,本地变量表的大小在编译的时候就已经确定了。

2.1.2 操作数栈

也就是执行操作指令的栈,栈里面存放指令所需要的参数,和保存指令返回的结果。操作数栈里面参数需要从本地变量表中加载,返回的结果如果需要保存在本地变量表中就通过指令保存,如果不需要这个结果,可以通过 pop 指令舍弃。

2.1.3 方法返回地址

2.1.4 动态链接

主要是invokeDynamic这个指令,1.8以后 labmda 表达式就会用到这个指令.

2.1.5 附加信息

2.2 字节码指令

字节码指令占用一个字节,也就是说最多也就256种指令。这里先给一个字节码指令表:shimo.im/sheets/GyrH...

在阅读字节码的时候,遇到不会的指令可以查一下。下面列出一些常见指令。

2.2.1 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,每个指令执行的参数必须在栈顶。

加载到操作数栈常见指令:iload(int, boolean, sort, char, byte), lload(long), fload(float), dload(double),aload(引用)

从操作数栈到本地变量表常见指令: istore(int, boolean, sort, char, byte), lstore(long), fstore(float), dstore(double),astore(引用)

2.2.2 运算指令

加减法指令:isub

乘法指令: imul

除法指令:idiv

求余指令: irem

取反指令: ineg

位移指令:ishl, ishr

或指令:ior

与指令:iand

异或指令:ixor

局部变量自增指令:iinc

2.2.3 类型转换指令

i2c, i2s, i2b, f2l

2.2.4 对象创建与访问指令

对象创建指令:new

数组创建指令: newarray, anewarray

访问字段指令: getfield, putfield

2.2.5 操作数栈指令

出栈指令: pop, pop2 (当调用一个函数的返回值不为空,但是你又不使用该变量,就应该调用这个指令让返回值出栈)

复制栈顶元素并重新入栈:dup, dup2

栈顶两个元素交换: swap

2.2.6 控制转移指令

条件分支:iflt,ifgt,ifeq,ifne

无条件分支: goto, jst

2.2.7 方法调用和返回指令

调用普通对象的public方法:invokevirtual

调用接口方法:invokeinterface

调用私有,父类,保护,初始化方法:invokespacial

调用静态方法:invokestatic

动态调用(Java8中的 lambda 用到了这个指令,有点复杂): invokedynamic

返回指令:return,lreturn, freturn, dreturn, areturn

3 与 Java 字节码相关的工具

3.1 javap

jdk 中提供的工具链,查看 class 字节码,也只能查看 class 字节码。

优点:稳定,查看单个 class 文件方便,能够查看的数据多。

缺点:一次只能查看单个文件,没有 UI,只能在控制台中查看

3.2 dexdump

Android 官方提供的查看 dex 文件中 class 文件信息

优点:能够直接查看 dex 文件

缺点:只能在控制台中运行没有 UI,输出的文本文件特别大,不是很推荐使用。

3.3 enjarify

Google官方出品, Github: github.com/Storyyeller...

能够将 apk或者 dex 中的 Dalvik VM 字节码等效地转换成 Jvm 字节码

优点:功能强大,是 Dex2Jar 的替代品。

缺点:少数类无法转换。(很少的类), 转换成 jar 的过程中会清除本地变量表,class 属性等一些描述信息,最大操作数栈信息也不对。

总的来说推荐使用,也没有更好的选择。

3.4 jadx

Github: github.com/skylot/jadx

能够将 apk, dex, jar, class 等多种类型文件反编译成源代码文件,就连 Android 中的资源文件都可以反编译,还有友好的 GUI 界面。

优点:功能强大,转换类型多,有 GUI,推荐使用。

缺点:-

3.5 jclasslib

Github:github.com/ingokegel/j...

能够查看jar和class文件的字节码,操作界面友好,能够同时查看多个class文件的字节码,还支持字节码的直接编辑,推荐使用。

4 使用工具查看 APK 中的字节码

4.1 使用enjarify工具将APK或者DEX转换成Jar包 (如果直接是jar或者是class这步跳过)

找到需要处理的apk文件

执行命令:
enjarify app-debug.apk

得到转换后的jar文件:
app-debug-enjarify.jar

4.2 使用jadx反编译成源码和字节码同步看 (可选)

执行命令:
jadx-gui app-debug-enjarify.jar

就可以直接看到 UI 了,然后找想看的类的反编译后的源代码就好。

4.3 使用 jclasslib 查看字节码

打开 jclasslib 软件将 4.1 中生成的jar包添加到 classpath,然后就可以找到需要查看的class 文件了。

对应的源码如下,我们只看 say() 方法:

Java 复制代码
package com.tans.androidbytecode;

public class Person {
    public final String name;
    
    // ...

    public void say(String words) {
        System.out.printf("%s say: %s\n", name, words);
    }
    // ....
}

找到我们类对应的 say() 方法,然后查看字节码如下:

perl 复制代码
0 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
3 astore_2
4 iconst_2
5 anewarray #4 <java/lang/Object>
8 astore_3
9 aload_0
10 getfield #19 <com/tans/androidbytecode/Person.name : Ljava/lang/String;>
13 astore 4
15 aload_3
16 iconst_0
17 aload 4
19 aastore
20 aload_3
21 iconst_1
22 aload_1
23 aastore
24 aload_2
25 ldc #45 <%s say: %s>
27 aload_3
28 invokevirtual #43 <java/io/PrintStream.printf : (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;>
31 pop
32 return

刚进入方法时本地变量表内容:

Slot name type
0 this(变量自己) Person
1 words(方法参数) String

执行下面指令:

kotlin 复制代码
0 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;> //获取静态变量 out, 入操作数栈
3 astore_2 //添加到本地变量表

获取静态变量 System.out,它的类型是 java.io.PrintStream,将 getstatic 指令的返回结果存入本地变量表 Slot 2 中。

本地变量表内容:

Slot name type
0 this(变量自己) Person
1 words(方法参数) String
2 out PrintStream

执行下面指令:

less 复制代码
4 iconst_2 //静态变量值2,入操作数栈
5 anewarray #4 <java/lang/Object> // 创建Object数组,大小为2,同时入操作数栈
8 astore_3 // 添加到本地变量表,slot 3

通过 iconst_2 将常量 2 加入操作数栈,调用 anewarray 指令创建大小为 2 的 Object 数组,然后存放到本地变量表 Slot 3 中。

本地变量表内容:

Slot name type
0 this(变量自己) Person
1 words(方法参数) String
2 out PrintStream
3 arrays Object2 (空数组)

执行下面指令:

arduino 复制代码
9 aload_0 // 本地变量表slot 0,入操作数栈,也就是this。
10 getfield #19 <com/tans/androidbytecode/Person.name : Ljava/lang/String;> //this.name成员变量,如操操作数栈.
13 astore 4 // 将name,入本地变量表slot 4

通过 aload_0 加载本地变量表中 slot 0 中的数据到操作数栈 (也就是 this,Persion 对象),然后调用 getfield 指令,加载 Persion#name 属性,类型是 String 到操作数栈,然后存放在本地变量表 slot 4 中。

本地变量表内容:

Slot name type
0 this(变量自己) Person
1 words(方法参数) String
2 out PrintStream
3 arrays Object2 (空数组)
4 name String

执行字节码指令:

arduino 复制代码
15 aload_3 // arrays从变量表中入栈
16 iconst_0 // 静态变量0,入栈
17 aload 4 // name,入栈
19 aastore // 执行数组中插入值,也就是array[0] = name
20 aload_3 // 下面的命令和上面类似
21 iconst_1
22 aload_1 // 参数中的words,入栈。
23 aastore // 修改数组中的值,array[1] = words

这段指令主要是用来设置 arrays 中的数据,aload_3 将 arrays 入栈,iconst_0 将常数 0 入栈,aload_4 将成员变量 name 入栈,调用 aastore 为数组设值,其实上面做了这么多操作就是将 name 赋值给 Object[0],后面也是类似的操作,只是把参数中的 words 赋值给 Object[1]。

本地变量表:

Slot name type
0 this(变量自己) Person
1 words(方法参数) String
2 out PrintStream
3 arrays Object2 (已经初始化值,0=name, 1=words)
4 name String

执行字节码指令:

javascript 复制代码
24 aload_2 // 从变量表加载out入栈
25 ldc #45 <%s say: %s> // 从常量池加载字符串入栈
27 aload_3 // 从变量表加载arrays入栈
28 invokevirtual #43 <java/io/PrintStream.printf : (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;> //调用out.printf方法
31 pop // 将printf的返回值出栈
32 return // 返回方法

aload_2 将 out 变量入栈,通过 ldc 指令从常量池中加载 %s say: %s 入栈,aload_3 将 arrays 入栈,最后执行 invokevirtual 方法,调用 out 变量的 printf 方法,方法的签名为 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;,由于我们没有用到 printf 方法的返回值,然后再调用 pop 指令,清空最上面的栈,我们的方法没有返回值,直接通过 return 返回就行了。

上面就完成了对一个方法的简单分析。

这里要注意一下:

本地变量表的空间是可以重复利用的,比如说 slot1 中的数据在后面的操作中是不会再使用的,新产生的数据是可以再放入 slot1 中的。比如说我要执行 int a = 1 + 1,执行这个操作就需要将两个加数入栈,假如分别是 slot0 和 slot1,执行完 iadd 指令后会产生新的值,这个时候 slot0 和 slot1 中的值就没有用了,新产生的值就不需要放在 slot2 中,直接放入 slot1 就好了。

猴,我相信你已经对 JVM 字节码已经懂一点点了,我们再去找一个场景练练手,那就选择 Android 环境下和 JVM 环境下的 lambda 对比。

5 对比 Android 和 JVM 中的 lambda 表达式

测试代码为:

MainActivity.kt

Kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        doSomething {
            println("Hello, World")
        }
    }

    private fun doSomething(f: TestInterface) {
        f.f()
    }
    
}

TestInterface.java

Java 复制代码
public interface TestInterface {
    void f();
}

5.1 Android 处理 Lambda

5.1.1 Labmda 中无其他引用

MainActivity.class onCreate方法字节码(忽略其他无关字节码)

swift 复制代码
11 getstatic #46 <com/tans/myapplication/-$$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc.INSTANCE : Lcom/tans/myapplication/-$$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc;>
14 astore_2
15 aload_0
16 aload_2
17 invokespecial #50 <com/tans/myapplication/MainActivity.doSomething : (Lcom/tans/myapplication/TestInterface;)V>

上面字节码表示:获取静态对象 com/tans/myapplication/-$$LambdaMainActivitywt_38GqJcEewnV1aO0Q7GXbL5Fc.INSTANCE,把这个对象当成参数传递给自己的doSomthing方法。

MainActivity.class doSomething方法:

bash 复制代码
0 aload_1
1 invokeinterface #13 <com/tans/myapplication/TestInterface.f : ()V> count 1
6 return

很简单直接调用传入对象的f方法.

com/tans/myapplication/-$$LambdaMainActivitywt_38GqJcEewnV1aO0Q7GXbL5Fc 对象的f方法:

bash 复制代码
0 invokestatic #20 <com/tans/myapplication/MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc : ()V>
3 return

同样很简单,又反过去调用MainActivity的静态方法MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc。

MainActivity.class MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc静态方法:

bash 复制代码
0 invokestatic #16 <com/tans/myapplication/MainActivity.onCreate$lambda-0 : ()V>
3 return

它又调用了自己的onCreate$lambda-0 方法

MainActivity.class onCreate$lambda-0 方法:

bash 复制代码
0 getstatic #22 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #24 <Hello, World>
5 invokevirtual #30 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
8 return

直接打印Hello,World到控制台上.

将上面的字节码反编译一下,大概如下:

Java 复制代码
public final class MainActivity extends AppCompatActivity {
    /* access modifiers changed from: protected */
    @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.fragment.app.FragmentActivity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        doSomething($$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc.INSTANCE);
    }

    /* access modifiers changed from: private */
    /* renamed from: onCreate$lambda-0  reason: not valid java name */
    public static final void m1onCreate$lambda0() {
        System.out.println((Object) "Hello, World");
    }

    private final void doSomething(TestInterface f) {
        f.f();
    }
}
Java 复制代码
public final /* synthetic */ class $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc implements TestInterface {
    public static final /* synthetic */ $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc INSTANCE = new $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc();

    private /* synthetic */ $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc() {
    }

    @Override // com.tans.myapplication.TestInterface
    public final void f() {
        MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc();
    }
}

5.1.2 Labmda 中有其他引用

如果我将 MainActivity 的代码改为如下:

Kotlin 复制代码
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val time = System.currentTimeMillis()
        doSomething {
            println("Hello, World, time: $time")
        }
    }

    private fun doSomething(f: TestInterface) {
        f.f()
    }

}

在Labmda中有引用外部的变量.

字节码:

MainActivity.class onCreate

swift 复制代码
11 invokestatic #57 <java/lang/System.currentTimeMillis : ()J>
14 lstore_2
15 new #59 <com/tans/myapplication/-$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw>
18 astore 4
20 aload 4
22 lload_2
23 invokespecial #61 <com/tans/myapplication/-$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw.<init> : (J)V>
26 aload_0
27 aload 4
29 invokespecial #65 <com/tans/myapplication/MainActivity.doSomething : (Lcom/tans/myapplication/TestInterface;)V>

和5.1.1中的不同的是com/tans/myapplication/-$$LambdaMainActivityV5EOAdm08AHNszPmNw9kNG0Rknw对象不是单例,而是直接new的一个对象,同时把我们需要的参数传递给该对象的构造函数.

com/tans/myapplication/-$$LambdaMainActivityV5EOAdm08AHNszPmNw9kNG0Rknw对象的f函数:

swift 复制代码
0 aload_0
1 getfield #14 <com/tans/myapplication/-$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw.f$0 : J>
4 invokestatic #20 <com/tans/myapplication/MainActivity.lambda$V5EOAdm08AHNszPmNw9kNG0Rknw : (J)V>
7 return

同样和5.1.1中的不同的是这里调用,多了一个参数。

其他都大同小异,直接看Labmda中的实现方法.

vbnet 复制代码
0 lload_0
1 invokestatic #23 <java/lang/Long.valueOf : (J)Ljava/lang/Long;>
4 astore_2
5 ldc #25 <Hello, World, time: >
7 aload_2
8 invokestatic #31 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
11 astore_2
12 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
15 aload_2
16 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
19 return

同样labmda实现的方法中也多了一个参数.

反编译后的代码大致如下:

Java 复制代码
public final class MainActivity extends AppCompatActivity {
    /* access modifiers changed from: protected */
    @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.fragment.app.FragmentActivity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        doSomething(new TestInterface(System.currentTimeMillis()) {
            /* class com.tans.myapplication.$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw */
            public final /* synthetic */ long f$0;

            {
                this.f$0 = r1;
            }

            @Override // com.tans.myapplication.TestInterface
            public final void f() {
                MainActivity.lambda$V5EOAdm08AHNszPmNw9kNG0Rknw(this.f$0);
            }
        });
    }

    /* access modifiers changed from: private */
    /* renamed from: onCreate$lambda-0  reason: not valid java name */
    public static final void m1onCreate$lambda0(long $time) {
        System.out.println((Object) Intrinsics.stringPlus("Hello, World, time: ", Long.valueOf($time)));
    }

    private final void doSomething(TestInterface f) {
        f.f();
    }
}

Labmda的最终实现会是在当前对象中生成一个静态方法. 如果该labmda内无其他参数的引用,会生成一个单例对象(自动生成,最终实现,会调用到Labmda最终实现的静态方法),反之就会new一个对象(自动生成,最终实现,会调用到Labmda最终实现的静态方法)。 所以,如果我们有意地在labmda中有意地不使用外部引用,某些情况下可以提升性能(少了new,和init函数的调用).

5.2 Java 1.8 处理 Lambda

MainActivity onCreate

bash 复制代码
11 aload_0
12 invokedynamic #36 <f, BootstrapMethods #0>
17 invokespecial #40 <com/tans/myapplication/MainActivity.doSomething : (Lcom/tans/myapplication/TestInterface;)V>
20 return

我们看到它既没有用单利,也没有new对象,而是调用了invokedynamic指令生成了一个对象,这个就留给读者去解决这个问题吧.

相关推荐
96245612 小时前
从“一键创建 ECS”到完整恢复状态机:ACK 抢占式节点恢复 Android 工具升级复盘
android
LCD液晶屏15 小时前
京瓷液晶屏停产替代:杭州立煌科技工业 LCD 解决方案
android·科技
小溪学编程15 小时前
从 C++ 的规范变迁看语言发展
jvm·c++·面试
晚安code16 小时前
JVM 类加载机制详解:双亲委派模型的原理、优势与三种打破方式
jvm
2501_9327502616 小时前
Android 自定义 View 入门
android
事圆则缓19 小时前
Android 开启线程的几种方式:Thread、线程池、HandlerThread 与协程
android
Joe_Wang519 小时前
【从0到1学习JVM · 21】既不浪费空间又没有碎片,标记整理算法到底慢在哪
jvm·学习·算法·垃圾回收·标记整理算法
Cheney Pan20 小时前
第4篇 Spring Boot 应用监控:Micrometer 与 JVM 指标
jvm·spring boot·后端·prometheus
Joe_Wang520 小时前
【从0到1学习JVM · 25】同样都要停顿所有线程,Parallel比Serial到底强在哪
java·jvm·学习·垃圾回收