一个class文件的完整格式
使用javap命令可以将一个二进制的字节码文件反编译为人类容易阅读的形式。分析class文件最关键弄清楚常量池每个常量含义和方法中每个指令含义。 对于如下java代码,使用javac我们将其编译为class文件
public class Test {
public static void main ( String[ ] args) {
System. out. println ( "hello" ) ;
}
}
然后使用命令 javap -v Test.class 可以得到如下信息,它包括了类基本信息 常量池 方法信息
Classfile / E: / projectstudy/ javasestudy/ src/ main/ java/ Test. class
Last modified 2020 - 9 - 25 ; size 466 bytes
MD5 checksum 575e1 dd61e5f0715a5c376086734f50a
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6. #17
#2 = Fieldref #18. #19
#3 = String #20
#4 = Methodref #21. #22
#5 = Class #23
#6 = Class #24
#7 = Utf8 < init>
#8 = Utf8 ( ) V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ( [ Ljava/ lang/ String; ) V
#13 = Utf8 Exceptions
#14 = Class #25
#15 = Utf8 SourceFile
#16 = Utf8 Test. java
#17 = NameAndType #7 : #8
#18 = Class #26
#19 = NameAndType #27 : #28
#20 = Utf8 hello
#21 = Class #29
#22 = NameAndType #30 : #31
#23 = Utf8 Test
#24 = Utf8 java/ lang/ Object
#25 = Utf8 java/ lang/ InterruptedException
#26 = Utf8 java/ lang/ System
#27 = Utf8 out
#28 = Utf8 Ljava/ io/ PrintStream;
#29 = Utf8 java/ io/ PrintStream
#30 = Utf8 println
#31 = Utf8 ( Ljava/ lang/ String; ) V
{
public Test ( ) ;
descriptor: ( ) V
flags: ACC_PUBLIC
Code:
stack= 1 , locals= 1 , args_size= 1
0 : aload_0
1 : invokespecial #1
4 : return
LineNumberTable:
line 2 : 0
public static void main ( java. lang. String[ ] ) ;
descriptor: ( [ Ljava/ lang/ String; ) V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack= 2 , locals= 1 , args_size= 1
0 : getstatic #2
3 : ldc #3
5 : invokevirtual #4
8 : return
LineNumberTable:
line 5 : 0
line 6 : 8
}
将一个数压入到操作数栈
bipush 将单字节的常量值( - 128 ~ 127 ) 推送至栈顶
sipush 将一个短整型常量值( - 32768 ~ 32767 ) 推送至栈顶
ldc 将int , float 或String型常量值从常量池中推送至栈顶
ldc_w 将int , float 或String型常量值从常量池中推送至栈顶(宽索引)
ldc2_w 将long 或double 型常量值从常量池中推送至栈顶(宽索引)
将本地变量表中一个数压入到栈顶
iload 将指定的int 型本地变量推送至栈顶
lload 将指定的long 型本地变量推送至栈顶
fload 将指定的float 型本地变量推送至栈顶
dload 将指定的double 型本地变量推送至栈顶
aload 将指定的引用类型本地变量推送至栈顶
-- -
iconst_0 将int 型0 推送至栈顶
iconst_1 将int 型1 推送至栈顶
iconst_2 将int 型2 推送至栈顶
iconst_3 将int 型3 推送至栈顶
将操作数栈的栈顶存入本地变量表
istore 将栈顶int 型数值存入指定本地变量
lstore 将栈顶long 型数值存入指定本地变量
fstore 将栈顶float 型数值存入指定本地变量
dstore 将栈顶double 型数值存入指定本地变量
astore 将栈顶引用型数值存入指定本地变量
计算相关指令
iadd 将栈顶两int 型数值相加并将结果压入栈顶
isub 将栈顶两int 型数值相减并将结果压入栈顶
imul 将栈顶两int 型数值相乘并将结果压入栈顶
idiv 将栈顶两int 型数值相除并将结果压入栈顶
irem 将栈顶两int 型数值作取模运算并将结果压入栈顶
ineg 将栈顶int 型数值取负并将结果压入栈顶
ishl 将int 型数值左移位指定位数并将结果压入栈顶
ishr 将int 型数值右(符号)移位指定位数并将结果压入栈顶
iushr 将int 型数值右(无符号)移位指定位数并将结果压入栈顶
-- -
iinc 将指定int 型变量增加指定值(i++ , i-- , i+= 2 )
i2l 将栈顶int 型数值强制转换成long 型数值并将结果压入栈顶
i2f 将栈顶int 型数值强制转换成float 型数值并将结果压入栈顶
i2d 将栈顶int 型数值强制转换成double 型数值并将结果压入栈顶
iinc 将指定int 型变量增加指定值(在局部变量表直接操作)
返回指令
ireturn 从当前方法返回int
areturn 从当前方法返回对象引用
return 从当前方法返回void
成员与方法指令
getstatic 获取指定类的静态域,并将其值压入栈顶
putstatic 为指定的类的静态域赋值
getfield 获取指定类的实例域,并将其值压入栈顶
putfield 为指定的类的实例域赋值
invokevirtual 调用实例方法 动态绑定
invokespecial 调用超类构造方法,实例初始化方法,私有方法 静态绑定
invokestatic 调用静态方法 静态绑定
invokeinterface 调用接口方法
invokedynamic 调用动态链接方法
创建对象
new 创建一个对象,并将其引用值压入栈顶
newarray 创建一个指定原始类型(如int , float , char …)的数组,并将其引用值压入栈顶
anewarray 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
条件判断相关指令
ifeq 当栈顶int 型数值等于0 时跳转
ifne 当栈顶int 型数值不等于0 时跳转
iflt 当栈顶int 型数值小于0 时跳转
ifge 当栈顶int 型数值大于等于0 时跳转
ifgt 当栈顶int 型数值大于0 时跳转
ifle 当栈顶int 型数值小于等于0 时跳转
if_icmpeq 比较栈顶两int 型数值大小,当结果等于0 时跳转
if_icmpne 比较栈顶两int 型数值大小,当结果不等于0 时跳转
if_icmplt 比较栈顶两int 型数值大小,当结果小于0 时跳转
if_icmpge 比较栈顶两int 型数值大小,当结果大于等于0 时跳转
if_icmpgt 比较栈顶两int 型数值大小,当结果大于0 时跳转
if_icmple 比较栈顶两int 型数值大小,当结果小于等于0 时跳转
if_acmpeq 比较栈顶两引用型数值,当结果相等时跳转
if_acmpne 比较栈顶两引用型数值,当结果不相等时跳转
goto 无条件跳转
赋值语句的字节码解读
public static void main ( String[ ] args) {
int a = 10 ;
int b = Short. MAX_VALUE+ 1 ;
int c = a + b;
System. out. println ( c) ;
}
Constant pool:
#1 = Methodref #7. #16
#2 = Class #17
#3 = Integer 32768
#4 = Fieldref #18. #19
#5 = Methodref #20. #21
#6 = Class #22
#7 = Class #23
#8 = Utf8 < init>
#9 = Utf8 ( ) V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ( [ Ljava/ lang/ String; ) V
#14 = Utf8 SourceFile
#15 = Utf8 Test. java
#16 = NameAndType #8 : #9
#17 = Utf8 java/ lang/ Short
#18 = Class #24
#19 = NameAndType #25 : #26
#20 = Class #27
#21 = NameAndType #28 : #29
#22 = Utf8 Test
#23 = Utf8 java/ lang/ Object
#24 = Utf8 java/ lang/ System
#25 = Utf8 out
#26 = Utf8 Ljava/ io/ PrintStream;
#27 = Utf8 java/ io/ PrintStream
#28 = Utf8 println
#29 = Utf8 ( I) V
{
public static void main ( java. lang. String[ ] ) ;
descriptor: ( [ Ljava/ lang/ String; ) V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack= 2 , locals= 4 , args_size= 1
0 : bipush 10
2 : istore_1
3 : ldc #3
5 : istore_2
6 : iload_1
7 : iload_2
8 : iadd
9 : istore_3
10 : getstatic #4
13 : iload_3
14 : invokevirtual #5
17 : return
}
i++与++i的字节码解读
通过底层字节码分析i++与++i的区别 测试代码如下
public static void fun1 ( ) {
int i = 1 ;
int a = i++ ;
}
public static void fun2 ( ) {
int i = 1 ;
int a = ++ i;
}
Classfile / E: / projectstudy/ javasestudy/ src/ main/ java/ Test. class
Last modified 2020 - 9 - 26 ; size 377 bytes
MD5 checksum 0 c1587c50ee803f5109d2072f28e0dba
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3. #14
#2 = Class #15
#3 = Class #16
#4 = Utf8 < init>
#5 = Utf8 ( ) V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ( [ Ljava/ lang/ String; ) V
#10 = Utf8 fun1
#11 = Utf8 fun2
#12 = Utf8 SourceFile
#13 = Utf8 Test. java
#14 = NameAndType #4 : #5
#15 = Utf8 Test
#16 = Utf8 java/ lang/ Object
public static void fun1 ( ) ;
descriptor: ( ) V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack= 1 , locals= 2 , args_size= 0
0 : bipush 100
2 : istore_0
3 : iload_0
4 : iinc 0 , 1
7 : istore_1
8 : return
public static void fun2 ( ) ;
descriptor: ( ) V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack= 1 , locals= 2 , args_size= 0
0 : bipush 100
2 : istore_0
3 : iinc 0 , 1
6 : iload_0
7 : istore_1
8 : return
就是因为load指令与iinc指令顺序不同造成了最终结果不同。
条件判断的字节码解读
public static void fun1 ( ) {
int a = 100 ;
if ( a== 100 )
System. out. println ( 1 ) ;
else
System. out. println ( 2 ) ;
}
public static void fun1 ( ) ;
descriptor: ( ) V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack= 2 , locals= 1 , args_size= 0
0 : bipush 100
2 : istore_0
3 : iload_0
4 : bipush 100
6 : if_icmpne 19
9 : getstatic #2
12 : iconst_1
13 : invokevirtual #3
16 : goto 26
19 : getstatic #2
22 : iconst_2
23 : invokevirtual #3
26 : return
循环控制的字节码解读
public static void fun1 ( ) {
int a = 0 ;
while ( a< 10 )
a++ ;
}
public static void fun1 ( ) ;
descriptor: ( ) V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack= 2 , locals= 1 , args_size= 0
0 : iconst_0
1 : istore_0
2 : iload_0
3 : bipush 10
5 : if_icmpge 14
8 : iinc 0 , 1
11 : goto 2
14 : return
静态代码块底层原理
java编译器会将一个类中的所有静态代码块和静态变量赋值语句根据顺序全部合并起来形成一个cinit方法在类第一次加载时执行。 分析如下代码
public class Test {
static int a = 10 ;
static {
a = 20 ;
}
static int b = 100 ;
static {
b = 200 ;
}
}
static int a;
descriptor: I
flags: ACC_STATIC
static int b;
descriptor: I
flags: ACC_STATIC
static { } ;
descriptor: ( ) V
flags: ACC_STATIC
Code:
stack= 1 , locals= 0 , args_size= 0
0 : bipush 10
2 : putstatic #2
5 : bipush 20
7 : putstatic #2
10 : bipush 100
12 : putstatic #3
15 : sipush 200
18 : putstatic #3
21 : return
构造代码块底层原理
Java编译器会将一个类中所有的构造代码块 普通成员赋值 构造方法 合并为一个方法在类实例化时调用 分析如下代码
public class Test {
int a = 10 ;
{
a = 20 ;
}
Test ( ) {
a = 30 ;
}
}
int a;
descriptor: I
flags:
Test ( ) ;
descriptor: ( ) V
flags:
Code:
stack= 2 , locals= 1 , args_size= 1
0 : aload_0
1 : invokespecial #1
4 : aload_0
5 : bipush 10
7 : putfield #2
10 : aload_0
11 : bipush 20
13 : putfield #2
16 : aload_0
17 : bipush 30
19 : putfield #2
22 : return
实例对象调用静态方法的底层分析
我们知道实例对象可以调用类所有的静态方法,那么实例对象调用静态方法和类直接调用静态方法有什么不同吗? 分析如下代码
public class Test {
public static void fun ( ) { }
public static void main ( String[ ] args) {
new Test ( ) . fun ( ) ;
}
}
Code:
stack= 2 , locals= 2 , args_size= 1
0 : new #2
3 : dup
4 : invokespecial #3
7 : astore_1
8 : aload_1
9 : pop
10 : invokestatic #4
13 : return
可以发现,使用实例对象调用静态方法时,先使用load将对象引用加载到操作数栈,然后将其弹出,再根据常量池数据找到静态方法调用。也就是说 load与pop指令完全是浪费的,没有任何意义。它比类直接调用静态方法多了两个指令,也就增加了一些性能开销。
catch语句的字节码指令分析
public static void main ( String[ ] args) {
int i = 0 ;
try {
i = 10 ;
} catch ( Exception e) {
i = 20 ;
}
}
对应字节码指令如下 会使用一个Exception table来进行异常对象的匹配规则
Code:
stack= 1 , locals= 3 , args_size= 1
0 : iconst_0
1 : istore_1
2 : bipush 10
4 : istore_1
5 : goto 12
8 : astore_2
9 : bipush 20
11 : istore_1
12 : return
Exception table:
from to target type
2 5 8 Class java/ lang/ Exception
public static void main ( String[ ] args) {
int i = 0 ;
try {
i = 10 ;
} catch ( Exception e) {
i = 20 ;
} finally {
i = 30 ;
}
}
对应字节码如下 可以看到,会将finally语句的代码分发到每一个可能执行的分支,保证finally一定会执行。
Code:
stack= 1 , locals= 4 , args_size= 1
0 : iconst_0
1 : istore_1
2 : bipush 10
4 : istore_1
5 : bipush 30
7 : istore_1
8 : goto 27
11 : astore_2
12 : bipush 20
14 : istore_1
15 : bipush 30
17 : istore_1
18 : goto 27
21 : astore_3
22 : bipush 30
24 : istore_1
25 : aload_3
26 : athrow
27 : return
Exception table:
from to target type
2 5 11 Class java/ lang/ Exception
2 5 21 any
11 15 21 any
synchronized语句的字节码分析
public static void main ( String[ ] args) {
synchronized ( Test. class ) {
int a = 10 ;
}
}
对应字节码如下 可以看到其实利用了异常对象表来控制整个流程,保证无论是否发生异常都会正常解锁
Code:
stack= 2 , locals= 4 , args_size= 1
0 : ldc #2
2 : dup
3 : astore_1
4 : monitorenter
5 : bipush 10
7 : istore_2
8 : aload_1
9 : monitorexit
10 : goto 18
13 : astore_3
14 : aload_1
15 : monitorexit
16 : aload_3
17 : athrow
18 : return
Exception table:
from to target type
5 10 13 any
13 16 13 any