class文件
平台无关性:字节码文件可以跨平台运行
语言无关性:虚拟机并不关心Class文件的来源是什么语言,只要它符合Class文件应有的结构就可以在java虚拟机中运行
Class文件是一组以8位字节位基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,整个Class文件中存储地内容几乎都是程序运行的必要数据,没有空隙存在.
Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表.
无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数可以用来描述数字,索引引用,数量值,或者按照UTF-8编码构成字符串
表是由多个无符号数或其他表作为数据项构成的符合数据类型,所有表都习惯性地以"_info"结尾.表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表
java文件到class文件的转化
字节码整体结构
高版本字节码文件无法在低版本虚拟机运行
案例1
public class MyTest1 { private int a = 1; public int getA() { return a; } public void setA(int a) { this.a = a; } } //使用javap -verbose 反编译 D:\study\知识体系\java基础\jvm\bytecode\target\classes\com\google\it>javap -verbose MyTest1.class Classfile /D:/study/知识体系/java基础/jvm/bytecode/target/classes/com/google/it/MyTest1.class Last modified 2019-6-23; size 473 bytes MD5 checksum 4396d60a6c90a3f6af5d8b081779940e Compiled from "MyTest1.java" public class com.google.it.MyTest1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool://常量池 #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/google/it/MyTest1.a:I #3 = Class #22 // com/google/it/MyTest1 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/google/it/MyTest1; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest1.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/google/it/MyTest1 #23 = Utf8 java/lang/Object { public com.google.it.MyTest1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 9: 0 line 10: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/google/it/MyTest1; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 13: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/google/it/MyTest1; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 17: 0 line 18: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/google/it/MyTest1; 0 6 1 a I } SourceFile: "MyTest1.java"
使用winhex16进制查看器 查看
右边为左边16进制数对应的acii码
- 使用
javap -verbose
命令分析一个字节码文件时,将会分析该字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息
- 魔数:所有.class字节码文件的前4个字节都是魔数,魔数固定值:0xCAFEBABE
- 魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号).这里的版本号为00 00 00 34(hex)对应10进制 0 ,52
- 常量池(constant):紧接着主版本号之后就是常量池入口,一个java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作时Class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中.常量池中主要存储两类常量:字面常量与符号引用.字面量入文本字符串,java中声明为final的常量值等. 而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等.
- 常量池的总体结构:java类所对应的常量池数量与常量池数组(常量表)这两部分共同构成.常量池数组则紧跟在常量池数量之后.常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型,结构都是不同的,长度当然也就不同;但是,每一种元素的第一个数据都是一个
ul
类型,该字节是个标识位,占据一个字节,jvm
在解析常量池时,会根据这个ul
类型获取元素的具体类型.值得注意的是,常量池数组中元素的个数=常量池数-1(其中0暂时不使用),目的是满足某系常量池索引值的数据在特定情况下需要表达不引用任何一个常量池的含义:根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值:所以,常量池的索引从1而非0开始.
- 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量,类型与顺序)与返回值.根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型使用字符L加对象的全限定名称来表示.为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示,如下所示:B -byte, C - char,D - double,F - float,I -
int
, J - long ,S - short,Z -boolean
,V -void, L - 对象类型,如Ljava/lang/String
;
- 对于数组类型来说,每一个维度使用一个前置的[来表示,如
int
[]被记录为[ I ,string 被记录为[[Ljava/lang/String
- 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述.参数列表按照参数的严格顺序放在一组()之内,如方法:
String getRealnamebyIdAndNickname(int id , String name)
的描述符为:
class字节码中的数据类型
- 表(数组):表是多个基本数据或其他表,按照既定顺序组成的大的数据集合.表是有结构的,它的结构体现在:组成表的成分所在的位置和顺宇都是已经严格定义好的
Access_Flag访问标志
0x 0021:是0x 0020和0x 0001的并集,表示ACC_PUBLIC与ACC_SUPER
字段表集合
字段表用于描述类和接口中声明的变量.这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量
方法表结构
- jvm预定义了部分attirbute,但是编译器自己也可以实现自己的attribute写入class文件中,供运行时使用
- 不同的attribute通过attribute_name_index来区分
Code结构
Code attribute的作用是保存该方法的结构,如所对应的字节码
- attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length
- max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
- max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
- code_length表示该方法所包含的字节码的字节数以及具体的指令码
- 具体字节码即是该方法被调用时,虚拟机所执行的字节码
- exception_table,这里存放的是处理异常的信息
- 每个exception_table表项由start_pc,end_pc,handler_pc,catch_type组成
- start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
- handler_pc表示处理异常的代码的开始处.catch_type表示会被异常处理的异常类型,它指向常量池里的一个异常类.当catch_type为0时,表示处理所有的异常
使用idea插件jclasslib
插件查看字节码结构信息
字节码分析程序
字节码分析synchronized
关键字
public class MyTest2 { String str = "Welcome"; private static Integer in = 10; private int x = 5; public static void main(String[] args) { MyTest2 myTest2 = new MyTest2(); myTest2.setX(8); in = 20; } private synchronized void setX(int x) { this.x = x; } private void test(String str) { synchronized (str) { System.out.println("hello world"); } } } private test(Ljava/lang/String;)V TRYCATCHBLOCK L0 L1 L2 null TRYCATCHBLOCK L2 L3 L2 null L4 LINENUMBER 25 L4 ALOAD 1 DUP ASTORE 2 MONITORENTER //进入监视器对象 L0 LINENUMBER 26 L0 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "hello world" INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L5 LINENUMBER 27 L5 ALOAD 2 MONITOREXIT//退出监视器 L1 GOTO L6 L2 FRAME FULL [com/google/it/MyTest2 java/lang/String java/lang/Object] [java/lang/Throwable] ASTORE 3 ALOAD 2 MONITOREXIT //发生异常退出监视器 L3 ALOAD 3 ATHROW L6 LINENUMBER 28 L6 FRAME CHOP 1 RETURN L7 LOCALVARIABLE this Lcom/google/it/MyTest2; L4 L7 0 LOCALVARIABLE str Ljava/lang/String; L4 L7 1 MAXSTACK = 2 MAXLOCALS = 4
字节码分析构造方法和静态字段
- 字节码文件中会将成员变量的赋值和构造代码块的执行放到(构造方法对应的字节码指令)中的引用父类构造的后面
- 成员变量的赋值和构造块代码的执行会优先构造方法中的自定义代码的执行
- 每个构造方法都会执行上述内容
public <init>()V//无参构造 L0 LINENUMBER 9 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V //父类无参构造器 L1 LINENUMBER 10 L1 ALOAD 0 LDC "Welcome" //对成员变量str赋值 PUTFIELD com/google/it/MyTest2.str : Ljava/lang/String; //对成员变量str赋值 L2 LINENUMBER 12 L2 ALOAD 0 ICONST_5 PUTFIELD com/google/it/MyTest2.x : I//对成员变量x赋值 RETURN L3 LOCALVARIABLE this Lcom/google/it/MyTest2; L0 L3 0 MAXSTACK = 2 MAXLOCALS = 1
添加构造代码块和自定义构造方法
public class MyTest2 { String str = "Welcome"; private static Integer in = 10; private int x = 5; { System.out.println("构造代码块"); } public MyTest2(){ } public MyTest2(int i){ System.out.println("构造方法"); } public static void main(String[] args) { MyTest2 myTest2 = new MyTest2(); myTest2.setX(8); in = 20; } private synchronized void setX(int x) { this.x = x; } private void test(String str) { synchronized (str) { System.out.println("hello world"); } } private static synchronized void test2(){ } } // access flags 0x1 public <init>()V L0 LINENUMBER 16 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V L1 LINENUMBER 10 L1 ALOAD 0 LDC "Welcome" PUTFIELD com/google/it/MyTest2.str : Ljava/lang/String; L2 LINENUMBER 12 L2 ALOAD 0 ICONST_5 PUTFIELD com/google/it/MyTest2.x : I L3 LINENUMBER 14 L3 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "\u6784\u9020\u4ee3\u7801\u5757"//构造代码块 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L4 LINENUMBER 18 L4 RETURN L5 LOCALVARIABLE this Lcom/google/it/MyTest2; L0 L5 0 MAXSTACK = 2 MAXLOCALS = 1 // access flags 0x1 public <init>(I)V L0 LINENUMBER 19 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V L1 LINENUMBER 10 L1 ALOAD 0 LDC "Welcome" PUTFIELD com/google/it/MyTest2.str : Ljava/lang/String; L2 LINENUMBER 12 L2 ALOAD 0 ICONST_5 PUTFIELD com/google/it/MyTest2.x : I L3 LINENUMBER 14 L3 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "\u6784\u9020\u4ee3\u7801\u5757"//构造代码块 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L4 LINENUMBER 20 L4 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "\u6784\u9020\u65b9\u6cd5"//构造方法 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L5 LINENUMBER 21 L5 RETURN L6 LOCALVARIABLE this Lcom/google/it/MyTest2; L0 L6 0 LOCALVARIABLE i I L0 L6 1 MAXSTACK = 2 MAXLOCALS = 2
静态字段的字节码分析
// access flags 0x2A private static synchronized test2()V L0 LINENUMBER 38 L0 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "static method" INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L1 LINENUMBER 39 L1 RETURN MAXSTACK = 2 MAXLOCALS = 0 // access flags 0x8 static <clinit>()V L0 LINENUMBER 11 L0 BIPUSH 10 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; PUTSTATIC com/google/it/MyTest2.in : Ljava/lang/Integer; //对应源码中 private static Integer in = 10;给静态字段赋值 RETURN MAXSTACK = 1 MAXLOCALS = 0 }
对象创建初始化顺序(字节码角度)
- 首次创建对象构成对类的首次主动使用触发类的初始化
- 一个类的初始化会首先初始化其父类
- 父类初始化开始,执行字节码中<方法 给父类静态字段赋值,执行父类静态代码块,父类初始化结束
- 子类初始化开始,执行字节码中<方法 给子类静态字段赋值,执行父类静态代码块,子类初始化结束
- 子类进入方法中,执行第一行父类的方法,初始化父类普通成员和构造代码块,并执行构造方法中自定义代码
- 初始化子类普通成员和构造代码块,并执行构造方法中自定义代码
- 对象创建完成
案例
public class A extends B{ { System.out.println("child construction run"); System.out.println(a=1); } private int a=1; private static int b = 4; static { System.out.println("child static run"); } public A(){ System.out.println("child construction method run"); } public static void main(String[] args) { A a = new A(); } } class B{ { System.out.println("parent construction run"); System.out.println(a=1); } private int a=1; private static int b = 4; static { System.out.println("parent static run"); } public B(){ System.out.println("parent construction method run"); } } //output: parent static run child static run parent construction run 1 parent construction method run child construction run 1 child construction method run
字节码分析this关键字和异常处理
- 对应java类中的每一个实例方法(非static方法),其在编译后所生成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个this,它位于方法的第一个参数位置处:这样,我们就可以在java的实例方法中使用this来去访问当前对象的属性以及其他方法.
- 这个操作是在编译期间完成的,即由
javac
编译器在编译的时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由JVM在调用实例方法时,自动向实例方法传入this参数.所以,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量
public class MyTest3 { public void test() { try { InputStream in = new FileInputStream("test.txt"); ServerSocket serverSocket = new ServerSocket(9999); serverSocket.accept(); }catch (FileNotFoundException ex){ } catch (IOException ex) { }catch (Exception ex){ } finally { System.out.println("finally"); } } } public void test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=4, args_size=1 //这个参数为this 0: new #2 // class java/io/FileInputStream
java字节码对于异常的处理方式:
- 统一采用异常表的方式来对异常进行处理
- 在
jdk 1.4.2
之前的版本中,并不是使用异常表的方式对异常进行处理的,而是采用特定的指令方式.
- 当异常处理存在finally语句块时,现代化的JVM采取的处理方式是将finally语句块的字节码拼接到每一个catch块后面,换句话说,程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码
栈帧(stack frame)
- 栈帧是一种用于帮助虚拟机执行方法调用方法执行的数据结构,栈帧本身是一种数据结构,封装了方法的局部变量表,动态链接信息,方法的返回地址以及操作数栈等信息.
- 符号引用,直接引用,有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析,另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为java的多态性
静态解析的4种情形:
- 静态方法
- 父类方法
- 构造方法
- 私有方法
以上4类方法称作非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用
调用方法字节码指令
invokeinterface
:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法
invokestatic
调用静态方法
invokespecial
:调用自己的私有方法,构造方法以及父类的方法.
invokevirtual
:调用虚方法,运行期动态查找的过程
invokedynamic
:动态调用方法
public class MyTest5 { public void test(Grandpa grandpa) { System.out.println("grandpa"); } public void test(Father father) { System.out.println("father"); } public void test(Son son) { System.out.println("son"); } public static void main(String[] args) { Grandpa g1 = new Father(); Grandpa g2 = new Son(); MyTest5 myTest5= new MyTest5(); myTest5.test(g1); myTest5.test(g2); } } class Grandpa { } class Father extends Grandpa { } class Son extends Father { } //output: //grandpa //grandpa
方法的静态分派
Grandpa g1 = new Father();
以上代码,g1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father. 结论:变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期方可确定对于
jvm
方法的重载是一种静态的行为,只会根据实参的静态类型匹配方法参数来决定调用哪个方法,编译期间就可以完全确定方法的动态分配
方法接收者:具体调用方法的对象
invokevirtual
字节码指令的多态查找流程- 到操作数的栈顶去寻找栈顶元素所指向对象的实际类型
- 如果寻找到与常量池中描述符和名称相同的方法,并且具有相应的访问权限,直接返回目标方法的直接引用
方法重载是静态的,是编译期行为;方法重写是动态的,是运行期行为
public class MyTest6 { public static void main(String[] args) { Fruit apple = new Apple();//运行期间符号引用解析为直接引用 Fruit orange = new Orange(); apple.test(); orange.test(); apple= new Orange(); apple.test(); } } class Fruit{ public void test(){ System.out.println("Fruit"); } } class Apple extends Fruit{ @Override public void test(){ System.out.println("Apple"); } } class Orange extends Fruit{ @Override public void test(){ System.out.println("Orange"); } } //输出 //Apple //Orange //Orange
针对于方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(
virtual method table,vtable
)针对于
invokeinterface
指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interfacemethod
table,itable
)针对多态的调用方式采用了虚方法表的索引来代替查找的过程
虚方法表的初始化是在类加载的连接阶段初始化的
现代jvm
执行java字节码的时候,通常会将解释执行和编译执行二者结合来进行.基于栈的指令集和基于寄存器的指令集
解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行该指令
编译执行,是通过及时编译器(Just in time,JIT)将字节码转换位本地机器码来执行,现代JVM会根据代码热点来生成相应的本地机器码.
基于栈的指令集与基于寄存器的指令集之间的关系
- JVM执行指令时所采取的方式是基于栈的指令集
- 基于栈的指令集主要的操作有入栈与出栈两种
- 基于栈的指令集的优势在于它可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构紧密关联的,无法做到可移植
- 基于栈的指令集的缺点在于完成相同的操作,指令数量要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由
cpu
来执行的,它是在高速缓冲区中进行执行的,速度要快很多,虽然虚拟机可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些
public int myCaculate() { int a = 1; int b = 2; int c = 3; int d = 4; int result = (a + b - c) * d; return result; }
字节码
public int myCaculate(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=6, args_size=1 //唯一参数为this 0: iconst_1 //将数字1放入操作数栈顶 1: istore_1 //将1弹出操作数栈存储到索引为1的局部变量表中,索引为0存储的为this 2: iconst_2 //将数字2放入操作数栈顶 3: istore_2 //将2弹出操作数栈存储到索引为2的局部变量表中 4: iconst_3 5: istore_3 6: iconst_4 7: istore 4 9: iload_1 //将局部变量表中索引为2的数压至栈顶 10: iload_2 11: iadd //将这两个数弹出操作数栈,执行两个整加法,将相加结果压入栈顶 12: iload_3 13: isub//将这两个数弹出操作数栈,执行两个整减法,将相减结果压入栈顶 14: iload 4 16: imul //将这两个数弹出操作数栈,执行两个整数相乘,将相乘结果压入栈顶 17: istore 5 19: iload 5 21: ireturn
字节码分析java动态代理机制
public interface Subject { public void request(); } public class RealSubject implements Subject{ @Override public void request() { System.out.println("From real subject"); } } public class DynamicSubject implements InvocationHandler { private Object sub; public DynamicSubject(Object obj) { this.sub = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before calling: " + method); method.invoke(this.sub, args); System.out.println("after calling" + method); return null; } } public class Client { public static void main(String[] args) { System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); RealSubject rs = new RealSubject(); InvocationHandler ds = new DynamicSubject(rs); Class<?> cls = rs.getClass(); Subject subject = (Subject) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), ds); subject.request(); System.out.println(subject.getClass()); System.out.println(subject.getClass().getSuperclass()); String s = "1"; } }