JVM(6)Class文件结构及字节码指令

1.JVM的无关性

  与平台无关性是建立在操作系统上,虚拟机厂商提供了许多可以运行在各种不同平台的虚拟机,他们都可以载入和执行字节码,从而实现程序的"一次编写,处处执行"
jdk下载
  各种不同平台的虚拟机与所有平台都统一使用的程序存储格式—字节码(ByteCode)是构成平台无关系的基石,也是语言无关性的基础.java虚拟机不和包括java在内的任何语言绑定,他只与"Class"文件这种特定的二进制文件格式所关联,Class文件中包含了java虚拟机指令集和符号表以及若干其他辅助信息.
在这里插入图片描述

2.Class类文件(了解即可)

  Java技术能够一直保持非常好的向后兼容性,这点Class文件结构的稳定性功不可没.Java以及发展到14版本,但是class文件结构的内容,绝大部分在JDK1.2时代就已经定义好了.虽然JDK1.2的内容比较古老,但是java发展经历了十余个大半本,但是每次基本上只是在原有结构基础上新增内容,扩充功能,并未对定义内容做修改.
  任何一个Class文件都对应这唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件形式存在(比如可以动态生成,或者直接送入类加载器中).
  Class文件是一组以8位字节为基础单位的二进制流
  Class文件结构这些内容在面试的时候很少有人问,因此大家学这个东西一定要当成一个兴趣去学.这个是自身内力提升的过程

2.1 工具介绍
2.1.1Sublime

查看16进制的编辑器

2.1.2 javap

  javap是JDK自带的反解析工具,它的作用是将.Class字节码文件解析成可读的文件格式.
  在使用javap时一般会添加 -v 参数,尽量多打印一些信息,同时,也可以使用 -p 参数,打印一些私有的字段和方法.

2.1.3 jclasslib

  如果不太习惯使用命令行的操作,还可以使用jclasslib, jclasslib是一个图形化的工具,能够更加直观的查看字节码中的内容.它还分门别类的对类中的各个部分进行了整理,非常的人性化.同时,它还提供了idea的插件,你可以从plugins中搜索到它.
jclasslib的下载地址:https://github.com/ingokegel/jclasslib

2.2 Class文件格式

从一个class文件开始,下图是一个java文件

package com.wanzi.test.ex6;
/**
 * 字节码分析
 */
public class ByteCode {
    public ByteCode(){
    }
}

我们使用Sublime这个工具打开.class文件
在这里插入图片描述
  整个class文件的格式就是一个二进制的字节流.
  各个数据项目严格按照顺序紧凑的排列在class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在.
  Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
  无符号数属于基本数据类型,以u1,u2,u4,u8来分别代表1个字节(一个字节是由两位16机制数组成,比如u2代表两个字节的四位16进制数),2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值.
  表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以"_info"结尾.表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表

2.3 Class文件格式详解

  Class的结构不像XML等描述性语言,由于它没有任何分隔符号,所以在其中的数据项,无论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变.
按顺序包括

2.3.1 魔数与Class文件的版本

  每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动.文件格式的制定者可以自由的选择魔数值,这要这个魔数值还没有被广泛采用过同时又不会引起混淆即可.(如上图class文件的头四个8位的16进制数 cafebabe)
  紧接着魔数的4个字节存储的是Class文件的版本号:前两个字节是次版本号(MinorVersion),后两个字节是主版本号(Major Version).java的版本号是从45开始的,JDK1.1之后的每个JDK大半本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件.
0000 0034 代表JDK1.8(16进制的34,换成10进制就是52)

2.3.2 常量池

  常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count).
与Java中语言语言习惯不一样的是,这个容量计数是从1开始而不是0开始的
在这里插入图片描述
  常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References).
  字面量比较接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值等.
  符号引用则属于编译原理方面的概念,包括了下面的三大类常量:
类和类接口的全限定名(Fully Qualified Name),字段的名称和描述,方法的名称和描述符

2.3.3 访问标志

  用于识别一些类或者接口层次的访问信息,包括: 这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等

2.3.4 类索引,父索引与接口索引集合

  这三项数据来确定这个类的继承关系.类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名.由于Java语言不允许多继承,所以父类索引只有一个,除了java.lang.Object之外,所以的java类都有父类,因此除了java.lang.Object之外,所有的java类的父类都不为0.接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中

2.3.5字段表集合

  描述接口或者类中声明的变量.字段(field)包括类级变量以及实例级变量.
  而字段叫什么名字,字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述.
  字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段.

2.3.6 方法表集合

  描述了方法的定义,但是方法里的java代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为"code"的属性里面.与字段集合相类的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息.但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器"“方法和实例构造器”"

2.3.7 属性表集合

  存储Class文件,字段表,方发表和自己的属性表集合,以用于描述某些场景专有的信息.如方法的代码就存储在Code属性表中.

3 深入JVM及时编译器JIT

3.1 解释执行与JIT

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

3.2 热点代码

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

3.3 热点探测

  在HotSpot 虚拟机中的热点探测是JIT优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是"热点方法".
  

3.4 方法调用计数器

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

3.5 回边计数器

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值