分析字节码文件泛解析工具
- javap jdk自带泛解析.class文件为可读格式
javap使用例子: javap -p -v HelloWorld.class : 打印该字节码文件私有变量及更多信息 javac中可以指定额外内容输出到字节码,常用的如下: javac -g:lines 强制生成LineNumberTable javac -g:vars 强制生成LocalVariableTable Javac -g 生成所有debug信息
- jclasslib 图形化工具,更直观查看字节码,分门别类的对类中各部分进行整理,可以将其插件与idea开发工具集成
jclasslib下载地址:https://github.com/ingokegel/jclasslib
类加载和对象创建时机
-
简单例子
class B{ private int a = 1234; static long C = 1111; public long test(long num){ long ret = this.a +num + C; } } public class A{ private B b = new B(); public static void main(String[] args){ A a= new A(); long num = 4321; long ret = a.b.test(num); System.out.println(ret); } }
-
类的初始化发生在类加载阶段,除了new,还有如下方式
1.class的newIntance方法 ,会通过反射机制调用构造器 2.Constructor类的newInstance方法,会调用构造器 3.反序列化,不调用构造器 4.使用Object的clone方法
-
类加载和对象创建流程
当虚拟机遇到一条new指令是,首先会检查该指令参数能否在(运行时)常量池中定位一个符号引用。然后检查该符号引用的类字节码是否加载、解析和初始化。如果没有,将执行对应的类加载过程. 如上面例子:A的对象a调用b.test()时,会触发 B 类加载 A和B会被加载到元空间的方法区,进入main方法后,讲给执行引擎执行。该执行过程在栈上完成,其中有几个重要区域:虚拟机栈,程序计数器等
-
执行A代码,调用private B b = new B(),就会触发B类加载
查看字节码
命令行查看字节码
-
编译A.java生成字节码(如用idea,可以直接将参数追加到VM options里面)
强制生成LIneNumberTable,LocalVariableTab javac -g:lines -g:vars A.java
-
查看字节码
使用javap查看A.java和B.java的字节码,输出行号,本地变量表信息,还会输出当前类用到的常量池信息 java -p -v A Javap -p -v B
-
字节码形式
1: invokespecial #1 // Method java/lang/Object."<init>":()V 2: #2 = Fieldref #6.#27 // B.a:I 3: #6 = Class #29 // B 4: #27 = NameAndType #8:#9 // a:I 5: #8 = Utf8 a 6: #9 = Utf8 I 可以看到第一行,对象初始化首先调用Object.init()方法,而不是<cinit> 第二行是拼接了# 13 和#14的内容
-
字节码特殊字符
基本类型:B-byte C-char D-double F-float I-int J-long S-short Z-boolean 特殊类型:V - void 数组类型(每一位都是要一个前置”[“字符来描述): [Ljava/lang/String;
可视化查看字节码
- jclasslib 查看常量池(内容存放于Metaspace区域,属于堆外内存)
常量池包含.class文件常量池,运行时常量池,String常量池,大多是一些静态内容
- 查看类和对象初始化方法,包含上面例子test方法代码区域
本地变量表的slot可复用。注意其index最大值为3,证明本地变量表最多同时能够存放4个变量 另外观察LineNumberTable选项。该属性的作用是描述源码行号和字节码行号(偏移量)对应关系,有了这些信息,在debug是,就能够获取发生异常代码源代码行号
例子中test函数执行过程
code区域解释
- 变量、参数
同时使用了成员变量a,静态变量C,输入参数num。内存是在虚拟机栈分配,方法如下 public long test(long num){ long ret = this.a +num + C; return ret; }
- 对应字节码
public long test(long); descriptor:(J)J flag:ACC_PUBLIC Code: stack=4(栈帧最大深度为4),locals=5,args_size=2(有个默认参数this) 0: aload_0 - 将第一个引用型局部变量推到操作数栈 1: getfield #2 //Field a:I - 获取第二个属性 4: i2l -将整型转为长整型 5: lload_1 6: ladd 7: getstatic #3 //Field C:J -获取第三个属性(c的类型为long) 10: ladd 11: lstore_3 - 存储第三个属性变量ret 12:lload_3 -加载第三个属性ret 13: lreturn -返回ret LineNumberTable: line 13:0 line 14:12 LocalVariableTable: Start Length Slot Name Siagnature 0 14 0 this LB; 0 14 1 num J 12 2 3 ret J
- 重要参数
上面stack字样 为4 (stack=4):表明test方法最大操作数栈深度为4,jvm运行时根据该数值来分配栈帧中操作栈的深度 locals变量存储量局部变量的存储空间。单位是slot(槽),可被重用。其中存放内容包括:this,方法参数,异常处理器参数,方法体中定义的局部变量 Arg_size为方法参数个数,每个方法都有隐藏参数this,故为2
字节码执行流程及含义
-
0: aload_0
将第 1 个引用型局部变量推到操作数栈,把this压入操作数栈 对于static方法,aload_0表述对方法第一个参数(本方法为隐藏参数this,为第一个参数)
-
1: getfield #2
将站定指定的对象的第2个实例域(Field)的值,压入栈顶。#2指成员变量a. #2 = Fieldref #6.#27 // B.a:I ... #6 = Class #29 // B #27 = NameAndType #8:#9 // a:I
-
i2l
将栈顶int类型数据转换为long类型,设计隐式类型转换,图中信息无变动
-
lload_1
将第一个局部变量(num)入栈.这里l表示long,同样是局部变量装载。会看到该位置局部变量一开始就已经有值
-
ladd
把栈顶两个long类型值出栈后相加,并将结果入栈
-
getstatic #3
根据偏移量获取静态属性值,并把该值push到操作数栈
-
ladd
再次咨询ladd
-
lstore_3
把栈顶logn型数值存入第4个局部变量(ret) slot为4,索引为3的就是ret变量
-
lload_3
与上面想法。上面变量存入,现在把该变量玉如虚拟机栈中
-
lreturn
从当前方法返回long类的ret变量
字节码指令列表
[字节码指令列表](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html)
test方法改动为直接return this.a+num+c
````
public long test(long num) {
return this.a + num + C;
}
````
- 字节码指令
0:aload_0 -加载第一个参数this 1:getfield #2 //Field a:I -第二个参数对象a入栈 4:i2l - int转为long 5:lload_1 -加载第第一个局部变量入栈(num) 6:ladd - 将栈顶两个long变量相加 7:getstatic #2 //Field C:J -根据偏移量获取静态属性值,并把该值push到操作数栈 10:ladd -再次将栈顶两个long型变量相加 11:lreturn -返回相加后的值