JVM-从编译到执行-Jclasslib

从编译到执行

流程图

JVM
javac
Java文件
class文件
类加载器
字节码解释器
执行引擎
OS硬件
Jit及时编译器
Java类库

其他语言在JVM中如何运行

  1. 虽然JVM叫Java虚拟机,但原理上其实他是基于class的。
  2. 任何一种语言只要能生成符合jvm要求的class文件就能被执行。
  3. java可以通过javac命令生成class文件,其他语言也能用自己的方式生成class文件

Class文件结构

底层结构

  • 底层:二进制字节流

  • 数据类型:u1 u2 u4 u8 和 _info 表类型

    • _info 的来源是hotspot源码中的写法
    • u1代表1个字节,8位,16进制有2个数
    • u2代表2个字节,16位,16禁止有4个数

查看工具

16进制工具
  • sublime
  • notepad++ (需要装HexEditor插件)
  • IDEA插件:BinEd

16进制文件案例

cafe babe 0000 0034 003f 0a00 0a00 2b08
002c 0900 0d00 2d06 4059 0000 0000 0000
0900 0d00 2e09 002f 0030 0800 310a 0032
0033 0700 340a 000d 0035 0900 0d00 3607
0037 0100 046e 616d 6501 0012 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0361 6765 0100 0149 0100 0673 616c 6172
7901 0001 4401 000d 436f 6e73 7461 6e74
......
字节码工具
  • javap,在idea中,在project文件视图中选中java文件,然后选"view > view bytecode"就是调用的这个命令

  • JBE -可以直接修改0

  • JClassLib–>IDEA插件。安装之后,在project文件视图中选中java文件,然后选"view > view bytecode with Jclasslib"

    • 推荐安装这个工具,他会将class信息归类,并且还能编辑,比上面的工具强大很多。
      在这里插入图片描述

构成

整体结构

https://www.processon.com/v/6470d2bd1527b726f809ded4

一般信息
魔数
  1. 每个Class文件的头4个字节称为魔数(Magic Number)
  2. 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
  3. Class文件魔数的值为0xCAFEBABE。如果一个文件不是以0xCAFEBABE开头,那它就肯定不是Java class文件。

很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或jpeg等在文件头中都存有魔数。使用魔数而不是使用扩展名是基于安全性考虑的,扩展名可以随意被改变!!!

主次版本号

紧接着魔数的4个字节是Class文件版本号,版本号又分为:

  1. 次版本号(minor_version): 前2字节,4个16进制,用于表示次版本号
  2. 主版本号(major_version): 后2字节,4个16进制,用于表示主版本号。

这个的版本号是随着jdk版本的不同而表示不同的版本范围的。Java的版本号是从45开始的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。

0X0034(对应十进制的50):JDK1.8
0X0033(对应十进制的50):JDK1.7
0X0032(对应十进制的50):JDK1.6
0X0031(对应十进制的49):JDK1.5  
0X0030(对应十进制的48):JDK1.4  
0X002F(对应十进制的47):JDK1.3  
0X002E(对应十进制的46):JDK1.2
0X表示16进制

类的访问标志

常量池之后的数据结构是访问标志(access_flags),2字节,16位,这个标志主要用于识别类或接口层次的访问信息,主要包括:

  • 是否final
  • 是否public,否则是private
  • 是否是接口
  • 是否可用invokespecial字节码指令
  • 是否是abstact
  • 是否是注解
  • 是否是枚举
类索引、父类索引、接口

这三项数据主要用于确定这个类的继承关系。

其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。(多实现单继承)

类索引(this_class),用于确定这个类的全限定名,占2字节

父类索引(super_class),用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节

接口索引计数器(interfaces_count),占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节

接口索引集合(interfaces),一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中

this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

常量池

紧接着魔数与版本号之后的是常量池入口.常量池简单理解为class文件的资源从库

  1. 是Class文件结构中与其它项目关联最多的数据类型
  2. 是占用Class文件空间最大的数据项目之一
  3. 是在文件中第一个出现的表类型数据项目

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。

从1开始计数。Class文件结构中只有常量池的容量计数是从1开始的,第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的意思,这种情况就可以把索引值置为0来表示(留给JVM自己用的)。但尽管constant_pool列表中没有索引值为0的入口,缺失的这一入口也被constant_pool_count计数在内。例如,当constant_pool中有14项,constant_poo_count的值为15。

常量池之中主要存放两大类常量

  1. 字面量: 1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
  2. 符号引用: 属于编译原理方面的概念,包括了下面三类常量:
  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

Java代码在进行Java编译的时候,并不像C和C++那样有"连接"这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。
constant_pool_count:占2字节,本例为0x0016,转化为十进制为22,即说明常量池中有21个常量(只有常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~21。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示
constant_pool:表类型数据集合,即常量池中每一项常量都是一个表,共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构如下表所示:

接口

上文已经介绍过,主要是interfaces_count和interfaces

字段

fields_count:字段表计数器,即字段表集合中的字段表数据个数,占2字节。本测试类其值为0x0001,即只有一个字段表数据,也就是测试类中只包含一个变量(不算方法内部变量)

fields:字段表集合,一组字段表类型数据的集合。字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量

在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。

方法

methods_count:方法表计数器,即方法表集合中的方法表数据个数。占2字节,其值为0x0002,即测试类中有2个方法

methods:方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样。

2个字节为属性计数器,其值为0x0001,说明这个方法的属性表集合中有一个属性(详细说明见后面“属性表集合”)
属性名称为接下来2个字节0x0009,指向常量池中第9个常量,Code。
接下来4个字节为0x0000002F,表示Code属性值的字节长度为47。
接下来2个字节为0x0001,表示该方法的操作数栈的深度最大值为1。
接下来2个字节依然为0x0001,表示该方法的局部变量占用空间为1。
接下来4个字节为0x00000005,则紧接着的5个字节0x2AB70001B1为该方法编译后生成的字节码指令。
接下来2个字节为0x0000,说明Code属性异常表集合为空。
接下来2个字节为0x0002,说明Code属性带有2个属性,
接下来2个字节0x000A即为Code属性第一个属性的属性名称,指向常量池中第10个常量:LineNumberTable。
接下来4个字节为0x00000006,表示LineNumberTable属性值所占字节长度为6。
接下来2个字节为0x0001,line_number_table中只有一个line_number_info表,start_pc为0x0000,line_number为0x0003,LineNumberTable属性结束。
接下来2位0x000B为Code属性第二个属性的属性名,指向常量池中第11个常量:LocalVariableTable。该属性值所占的字节长度为0x0000000C=12。
接下来2位为0x0001,说明local_variable_table中只有一个local_variable_info表,按照local_variable_info表结构,start_pc为0x0000,length为0x0005,name_index为0x000C,指向常量池中第12个常量:this,descriptor_index为0x000D,指向常量池中第13个常量:LTestClass;,index为0x0000。

如果子类没有重写父类的方法,方法表集合中就不会出现父类方法的信息;有可能会出现由编译器自动添加的方法(如:最典型的,实例类构造器)在Java语言中,重载一个方法除了要求和原方法拥有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名(,由于特征签名不包含返回值,故Java语言中不能仅仅依靠返回值的不同对一个已有的方法重载;但是在Class文件格式中,特征签名即为方法描述符,只要是描述符不完全相同的2个方法也可以合法共存,即2个除了返回值不同之外完全相同的方法在Class文件中也可以合法共存。

注意:Java代码的方法特征签名只包括方法名称、参数顺序、参数类型。 而字节码的特征签名还包括方法返回值和受异常表。

属性

用于描述某些场景专有的信息,如字节码的指令信息等等。

集合中各属性表没有严格的顺序;

可以自定义属性信息,JVM会忽略不认识的属性表;

在Class文件的 ClassFile结构、字段表、方法表中都可以存储放自己的属性表集合,所以并不像最前面那Class文件结构那么直观,即属性不都是放在Class文件的最后:

五个属性对于Java虚拟机正确解释Class文件至关重要:

ConstantValue、Code、StackMapTable、Exceptions、BootstrapMethods;

十二个属性对于通过Java SE平台的类库来正确解释Class文件至关重要:

InnerClasses、EnclosingMethod、Synthetic、Signature、RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations、RuntimeInvisibleParameterAnnotations、RuntimeVisibleTypeAnnotations、RuntimeInvisibleTypeAnnotations、AnnotationDefault、MethodParameters;

六个属性对于通过Java虚拟机或Java SE平台的类库来正确解释Class文件并不重要,但对于工具非常有用

SourceFile、SourceDebugExtension、LineNumberTable、LocalVariableTable、LocalVariableTypeTable、Deprecated;

类执行

Java的类被加载到内存后,默认通过混合模式进行执行。即:解释器+热点代码编译的方式。

解释器:bytecode interpreter

编译器:JIT,Just in time compiler

起始阶段使用解释器执行。通过热点代码检测机制来动态判断需要编译的代码。

  • 方法计数器,检测方法调用频率。
  • 循环计数器,检测循环调用频率。

对于判断出来需要编译,jvm会将他编译成为本地代码。windows是exe,linux是elf。

相关配置项

  • -Xmixed:混合模式
  • -Xint:解释模式,启动很快,执行稍慢。
  • -Xcomp:编译模式,启动贼慢,执行很快。

代码测试

分别使用3种模式执行下列代码。(idea中,可以打开运行配置界面,在vm option中加入-Xint)

混合模式下,大概需要4秒左右,当执行第一遍循环后,jvm会将循环种的代码编译成本地代码,第二次在循环就会变快。

解释模式下,这段代码远远不止4秒。

编译模式下,大概需要3秒,启动稍慢,但后面执行就会变快。

public class TestClassRun {
    public static void main(String[] args) {
        for(int i=0; i<10_0000; i++)
            m();

        long start = System.currentTimeMillis();
        for(int i=0; i<10_0000; i++) {
            m();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static void m() {
        for(long i=0; i<10_0000L; i++) {
            long j = i%3;
        }
    }
}

字节码指令

store

load

pop

mul

sub

invoke

  1. InvokeStatic
  2. InvokeVirtual
  3. InvokeInterface
  4. InovkeSpecial
    可以直接定位,不需要多态的方法
    private 方法 , 构造方法
  5. InvokeDynamic
    JVM最难的指令
    lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令

参考说明

本文章内容为个人笔记,部分内容来源于马士兵课程视频学习笔记。部分参考自其他相关博客。如有错误欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值