java|JVM之字节码如何在jvm流转

分析字节码文件泛解析工具

  • 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){ Aa= 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类加载
    java|JVM之字节码如何在jvm流转
    文章图片

查看字节码 命令行查看字节码
  • 编译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."":()V 2:#2 = Fieldref#6.#27// B.a:I 3:#6 = Class#29// B 4: #27 = NameAndType#8:#9// a:I 5:#8 = Utf8a 6:#9 = Utf8I 可以看到第一行,对象初始化首先调用Object.init()方法,而不是 第二行是拼接了# 13 和#14的内容

  • 字节码特殊字符
    基本类型:B-byteC-char D-double F-float I-intJ-long S-shortZ-boolean 特殊类型:V - void 数组类型(每一位都是要一个前置”[“字符来描述): [Ljava/lang/String;

可视化查看字节码
  • jclasslib 查看常量池(内容存放于Metaspace区域,属于堆外内存)
常量池包含.class文件常量池,运行时常量池,String常量池,大多是一些静态内容

java|JVM之字节码如何在jvm流转
文章图片

  • 查看类和对象初始化方法,包含上面例子test方法代码区域
    本地变量表的slot可复用。注意其index最大值为3,证明本地变量表最多同时能够存放4个变量 另外观察LineNumberTable选项。该属性的作用是描述源码行号和字节码行号(偏移量)对应关系,有了这些信息,在debug是,就能够获取发生异常代码源代码行号

java|JVM之字节码如何在jvm流转
文章图片

例子中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: StartLengthSlot NameSiagnature 0140thisLB; 0141numJ 1223retJ

  • 重要参数
    上面stack字样 为4 (stack=4):表明test方法最大操作数栈深度为4,jvm运行时根据该数值来分配栈帧中操作栈的深度 locals变量存储量局部变量的存储空间。单位是slot(槽),可被重用。其中存放内容包括:this,方法参数,异常处理器参数,方法体中定义的局部变量 Arg_size为方法参数个数,每个方法都有隐藏参数this,故为2

字节码执行流程及含义
  • 0: aload_0
    将第 1 个引用型局部变量推到操作数栈,把this压入操作数栈 对于static方法,aload_0表述对方法第一个参数(本方法为隐藏参数this,为第一个参数)

  • 【java|JVM之字节码如何在jvm流转】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-返回相加后的值

    推荐阅读