从Class文件的角度看代码

如果JVM是Windows,那么Class就是Exe

Posted by Timer on December 19, 2021

[TOC]

Class文件结构

Class文件由两种数据类型组成,无符号数和表。

  • 无符号数:基本类型,由u1 u2 u3 u4 u8 代表对应的字节数。
  • 表:多个无符号数或者其他表构成的复合类型,由“_info” 结尾。

Class文件格式:

类型 名称 作用
u4 magic 确认文件是Class文件
u2 minor_version 次版本号
u2 major_version 主版本号
u2 constant_pool_count 常量池数量
cp_info constant_pool 常量池
u2 access_flags 访问修饰符
u2 this_class 类索引,用于确定这个类的全限定名
u2 super_class 父类索引,用于确定父类的全限定名
u2 interfaces 和上面两个一起确定这个类的继承关系
u2 interfaces_count 接口数量
field_info fields 字段表
u2 fields_count 字段表数量
method_info methods 方法表
u2 methods_count 方法表数量
attribute_info attributes 属性表,存额外信息,比如异常列表等等
u2 attributes_count 属性表数量

常用字节码命令:

操作码 作用
bipush int值入栈
iconst_0 0(int)入栈
iconst_1 同理,1(int)入栈
ldc 常量池中数据入栈
aload_x 把局部变量表中的x槽位的引用入栈
iload_x 把局部变量表中槽位x的int入栈
innc 把整数值constbyte加到indexbyte指定的int类型的局部变量里
istore_x 把int保存到局部变量x中
iadd 将栈顶两int类型数相加,结果入栈。

实际运用1:

int x = 10;
int ans = x++ + ++x + x-- + --x;

结论:从字节码的角度看出,x++和x–的区别在于:先自增还是先入操作数栈

操作 解释 操作数栈(左栈底,右栈顶) 局部变量表
bipush 10入操作数栈(后续简称为栈 10  
istore_1 把栈顶的10放到局部变量表1的位置   1:10
iload_1 把局部变量表下标为1的数装载入栈 10 1:10
iinc 1 1 把局部变量表下标为1的数自增1 10 1:11
iinc 1 1 把局部变量表下标为1的数自增1 10 1:12
iload_1 把局部变量表下标为1的数装载入栈 10 12 1:12
iadd 将栈顶10和12相加,结果入栈 22 1:12
iload_1 把局部变量表下标为1的数装载入栈 22 12 1:12
iinc 1, -1 把局部变量表下标为1的数自增-1 22 12 1:11
iadd 将栈顶22和11相加,结果入栈 34 1:11
iinc 1, -1 把局部变量表下标为1的数自增-1 34 1:10
iload_1 把局部变量表下标为1的数装载入栈 34 10 1:10
iadd 将栈顶33和10相加,结果入栈 44 1:10
istore_2 把栈顶的43放到局部变量表2的位置   1:10 2:44
return 返回   1:10 2:44

实际运用2:

int x = 10;
for (int i = 0; i < 10; i++) {
    x = x++;
}
System.out.println(x);

结论:

x++操作是先入栈再增加局部变量表,这意味着局部变量表改变了,栈顶数值没变。

等号又是把栈顶元素存储到局部变量表,所以整体无变化。

操作 操作数栈(左栈底,右栈顶) 局部变量表
bipush 10 10  
istore_1   1:10
iconst_0 0  
istore_2   1:10 2:0
iload_2 0  
bipush 10 0 10  
if_icmpge 22 0 10 1:10 2:0
iload_1 0 10 10  
iinc 1,1 0 10 10 1:11 2:0
istore_1 0 10 1:10 2:0
innc 2,1 0 10 1:10 2:1
goto 5 0 10 1:10 2:1
22: return    

实际运用3:

public class Test {
    static int i = 10;

    static {
        i = 40;
    }

    static {
        i = 30;
    }
}

结论:静态代码块的执行单纯是对每个块的位置由上到下顺序执行。

操作
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 40
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return

实际应用4:

public class Test {

    public Test(String a, int b) {
        this.a = a;
        this.b = b;
    }

    private int b = 10;

    {
        a = "s2";
    }

    private String a = "s1";

    {
        b = 20;
    }

    public static void main(String[] args) {
        Test t = new Test("s3", 30);
        System.out.println(t.a);
        System.out.println(t.b);
    }
}

结论:编译器会按从上到下的顺序,收集所有静态代码块和成员变量赋值的代码,行程新的构造方法,但是原始构造方法总会最后执行。

实际应用5:

public class Test {
    public Test() {}
    private void test1() {}
    private final void test2() {}
    public static void test3() {}
    public void test4() {}
    public static void main(String[] args) {
        Test test = new Test();
        test.test1();
        test.test2();
        test3();
        test.test4();
    }
}
方法对应的字节码
4: invokespecial #3 // Method “":()V
9: invokespecial #4 // Method test1:()V
13: invokespecial #5 // Method test2:()V
16: invokestatic #6 // Method test3:()V
20: invokevirtual #7 // Method test4:()V

invokespecial 和 invokestatic 是静态绑定,字节码生成的时候就知道自己要调用的对象。

invokevirtual是动态绑定,运行的时候才知道自己要调用的对象。因为从语法的角度来说,普通public方法是有可能被重写的。

实际应用6:

为什么这段代码不报异常?

public class Test {

    public int fun() {
        int x = 0;
        try {
            int value = 1 / 0;
            return x + 5;
        } finally {
            return x + 10;
        }
    }

    public static void main(String[] args) {
        int fun = new Test().fun();
        System.out.println(fun);
    }
}

结论:

从下方字节码可以观察出,如果finally里有return语句,则会屏蔽try和catch里的return语句。

另外,当try后面没有catch语句的时候,class文件会生成一个默认的异常表,target为finally代码块。

如果try里触发异常,会从异常的行直接跳到target。

image-20211220200349934

####

实际应用7:

public class Test {
    public static void main(String[] args) {
        Integer timer = 1;
        synchronized (timer) {
            System.out.println(1);
        }
    }
}

可以看出synchronized实现是用两个monitorenter实现,一个是用于正常状态的锁执行,另一个用于异常情况加载备份的锁。

image-20211221001615473