JVM_5class文件结构及深入字节码指令

JVM的无关性

在这里插入图片描述

查看字节码指令工具

  1. Sublime 查看 16 进制的编辑器
  2. javap
        javap 是 JDK 自带的反解析工具。
        它的作用是将 .class 字节码文件解析成可读的文件格式。 在使用 javap 时我一般会添加 -v 参数,尽量多打印一些信息。同时,我也会使用 -p 参数,打印一些私有的字段和方法。
  3. jclasslib
        idea插件

class字节码组成

  1. 魔数与 Class 文件的版本
  2. 常量池
  3. 访问标志
  4. 类索引、父类索引与接口索引集合
  5. 字段表集合
  6. 方法表集合
  7. 属性表集合
    在这里插入图片描述

字节码指令

  1. 加载和存储指令
        用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。
        将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
        将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n >。
        将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
        扩充局部变量表的访问索引的指令:wide。

  2. 运算指令
        用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
        加法指令:iadd、ladd、fadd、dadd。
        减法指令:isub、lsub、fsub、dsub。
        乘法指令:imul、lmul、fmul、dmul 等等

  3. 类型转换指令
        可以将两种不同的数值类型进行相互转换, Java 虚拟机直接支持以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换
        int    类型到 long、float 或者 double 类型。
        long 类型到 float、double 类型。
        float 类型到 double 类型。
        处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。

  4. 对象创建与访问指令
        创建对象:new
        创建数组的指令:newarray、anewarray、multianewarray。
        访问字段指令:getfield、putfield、getstatic、putstatic。
        数组存储指令:把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
        将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
        取数组长度的指令:arraylength。
        检查类实例类型的指令:instanceof、checkcast。

  5. 操作数栈管理指令
        如同操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:将操作数栈的栈顶一个或两个元素出栈:pop、 pop2。
        复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
        将栈最顶端的两个数值互换:swap。

  6. 控制转移指令
        控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控 制转移指令就是在有条件或无条件地修改 PC 寄存器的值。控制转移指令如下。
        条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
        复合条件分支:tableswitch、lookupswitch。
        无条件分支:goto、goto_w、jsr、jsr_w、ret。

  7. 方法调用指令
        invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
        invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
        invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
        invokestatic 指令用于调用类方法(static 方法)。
        invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面 4 条调用指令的分派逻辑都固化在 Java 虚拟机内部,而     invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。
        方法调用指令与数据类型无关。

  8. 方法返回指令
        是根据返回值的类型区分的,包括 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有 一条 return 指令供声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用。

  9. 异常处理指令
        在 Java 程序中显式抛出异常的操作(throw 语句)都由 athrow 指令来实现

  10. 同步指令
        有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义

字节码剖析异常处理

完成出口(返回地址): 正常返回:(调用程序计数器中的地址作为返回)
三步曲:
    恢复上层方法的局部变量表和操作数栈、
    把返回值(如果有的话)压入调用者栈帧的操作数栈中、
    调整程序计数器的值以指向方法调用指令后面的一条指令、
    异常的话:(通过异常处理表<非栈帧中的>来确定)
在这里插入图片描述
异常表
示例
工具 :javap -v java.class

    在 synchronized 生成的字节码中,其实包含两条 monitorexit 指令,是为了保证所有的异常条件,都能够退出。 可以看到,编译后的字节码,带有一个叫 Exception table 的异常表,里面的每一行数据,都是一个异常处理器:
     from     指定字节码索引的开始位置
     to          指定字节码索引的结束位置
     target   异常处理的起始位置
     type      异常类型

    也就是说,只要在 from 和 to 之间发生了异常,就会跳转到 target 所指定的位置。
我可以看到,第一条 monitorexit(16)在异常表第一条的范围中,如果异常,能够跳转到第 20 行 第二条 monitorexit(22)在异常表第二条的范围中,如果异常,能够跳转到第 20 行

Finally 通常我们在做一些文件读取的时候,都会在 finally 代码块中关闭流,以避免内存的溢出。(异常表有兴趣可以自己做测试)

Integer 的自动装箱拆箱

使用Integer 默认为null,而int默认为0
通过观察字节码,我们发现:
     1、在进行乘法运算的时候,调用了 Integer.intValue 方法来获取基本类型的值。
     2、赋值操作使用的是 Integer.valueOf 方法。
     3、在方法返回的时候,再次使用了 Integer.valueOf 方法对结果进行了包装。

这就是 Java 中的自动装箱拆箱的底层实现。

注意:
IntegerCache integer缓存,默认-128 ~ 127,通过 -XX:AutoBoxCacheMax 来修改上限,下线不能修改。

Integer面试题:

在这里插入图片描述
     解析:
     一般情况下是是 true,false 因为缓存的原因。(在缓存范围内的值,返回的是同一个缓存值,不在的话,每次都是 new 出来的)
     当我加上 VM 参数 -XX:AutoBoxCacheMax=256 执行时,结果是 true,ture,扩大缓存范围,第二个为 true 原因就在于此。

字节码指令-数组

     其实,数组是 JVM 内置的一种对象类型,这个对象同样是继承的 Object 类。我们使用代码来理解一下
在这里插入图片描述
在这里插入图片描述

数组创建
     可以看到,新建数组的代码,被编译成了 newarray 指令
在这里插入图片描述
数组里的初始内容,被顺序编译成了一系列指令放入:
     sipush 将一个短整型常量值推送至栈顶;
     iastore 将栈顶 int 型数值存入指定数组的指定索引位置。
在这里插入图片描述
具体操作:
     1、 iconst_0,常量 0,入操作数栈
     2、 sipush 将一个常量 1111 加载到操作数栈
     3、 将栈顶 int 型数值存入数组的 0 索引位置

     为了支持多种类型,从操作数栈存储到数组,有更多的指令:bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore。

数组访问
在这里插入图片描述
数组元素的访问,是通过第 28 ~ 30 行代码来实现的:
 aload_1 将第二个引用类型本地变量推送至栈顶,这里是生成的数组;
 iconst_2 将 int 型 2 推送至栈顶;
 iaload 将 int 型数组指定索引的值推送至栈顶。

获取数组的长度,是由字节码指令 arraylength 来完成的

在这里插入图片描述
获取数组长度的指令 arraylength

字节码指令——foreach

无论是 Java 的数组,还是 List,都可以使用 foreach 语句进行遍历,虽然在语言层面它们的表现形式是一致的,但实际实现的方法并不同

在这里插入图片描述
    数组:它将代码解释成了传统的变量方式,即 for(int i;i<length;i++) 的形式。
    List 的它实际是把 list 对象进行迭代并遍历的,在循环中,使用了 Iterator.next() 方法。

使用 jd-gui 等反编译工具,可以看到实际生成的代码:
在这里插入图片描述

字节码指令——注解

在这里插入图片描述
在这里插入图片描述

    无论是类的注解,还是方法注解,都是由一个叫做 RuntimeInvisibleAnnotations 的结构来存储的
    而参数的存储,是由 RuntimeInvisibleParameterAnotations 来保证的

字节码指令总结

    Java 的特性非常多,这里不再一一列出,但都可以使用这种简单的方式,从字节码层面分析了它的原理,一窥究竟。
    以上操作属于抛砖引玉,给出了大家一种学习思路。
    比如异常的处理、finally 块的执行顺序;以及隐藏的装箱拆箱和 foreach 语法糖的底层实现。
    还有字节码指令,可能有几千行,看起来很吓人,但执行速度几乎都是纳秒级别的。Java 的无数框架,包括 JDK,也不会为了优化这种性能对代码进行 限制。了解其原理,但不要舍本逐末,比如减少一次 Java 线程的上下文切换,就比你优化几千个装箱拆箱动作,速度来的更快一些。

深入 JVM 即时编译器 JIT

解释执行与 JIT
    Java 程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行,解释执行的方式是非常低效的,它需要把字 节码先翻译成机器码,才能往下执行。另外,字节码是 Java 编译器做的一次初级优化,许多代码可以满足语法分析,其实还有很大的优化空间。 所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。 完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。

    热点代码
    热点代码,就是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码。这些再次编译后的机器码会被缓存起来,以备下次使用,但对于那些执行次数很少的代码来说,这种编译动作就纯属浪费。
    JVM 提供了一个参数“-XX:ReservedCodeCacheSize”,用来限制 CodeCache 的大小。也就是说,JIT 编译后的代码都会放在 CodeCache 里。
    如果这个空间不足,JIT 就无法继续编译,编译执行会变成解释执行,性能会降低一个数量级。同时,JIT 编译器会一直尝试去优化代码,从而造成了 CPU 占用上升。

    热点探测
    在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执 行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
    虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这 两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT 编译。

方法调用计数器
    用于统计方法被调用的次数,方法调用计数器的默认阈值在 C1 模式下是 1500 次,在 C2 模式在是 10000 次,可通过 -XX: CompileThreshold 来设定; 而在分层编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边 计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。

回边计数器
    用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),该值用于计算是否触发 C1 编译的阈值, 在不开启分层编译的情况下,C1 默认为 13995,C2 默认为 10700,可通过 -XX:OnStackReplacePercentage=N 来设置;而在分层编译的情况下,-XX:OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。
    建立回边计数器的主要目的是为了触发OSR(OnStackReplacement)编译,即栈上编译。在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语 言。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值