[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 “ |
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。
####
实际应用7:
public class Test {
public static void main(String[] args) {
Integer timer = 1;
synchronized (timer) {
System.out.println(1);
}
}
}
可以看出synchronized实现是用两个monitorenter实现,一个是用于正常状态的锁执行,另一个用于异常情况加载备份的锁。