代码编译的结果从本地机器码转为字节码,是储存格式发展的一小步,却是编程语言的一大步。——《深入理解Java虚拟机》

计算机只认识0和1.所以我们写的编程语言只有转义成二进制本地机器码才能让机器认识。然而随着虚拟机的发展,包括Java在内的很多语言,都选择了一种和操作系统、机器指令集无关的中立储存格式来储存编译后的数据。

无关性

我们都知道Java经典标语,“一次编译,到处运行”。实现这一目标,每个平台上定制的虚拟机,需要读取统一的数据。这种数据不依赖于任何一种平台,甚至不关心是由哪种语言编译来的,只要统一了格式,虚拟机就能正确的使用它。这种统一的格式就是——字节码(Class文件)。

Class文件中储存了Java虚拟机指令集和符号表以及若干其他辅助和结构化约束。处于安全考虑,Class文件中使用了许多强制性的语法和结构化约束。

Class类文件的结构

下面来看下本文的硬菜,Class文件的结构。虽说大佬书中是以JDK1.4为版本讲述的,但是它所包含的指令、属性是Class文件中最重要最基础的。后续不同的版本都是对它的增强。

任何一个Class文件都对应着唯一一个类或者接口的定义信息,但是反过来说,类和接口并不一定都得定义在文件里(譬如类和接口也可以通过类加载器直接生成)。

Class文件是以一组以8位字节为基础单位的二进制流,这个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中储存的内容几乎是程序运行的必要数据,没有空格存在。

Class有两种数据类型(虽然用十六进制编辑器打开,看上去都是十六进制字符):无符号数和表。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构造成的符合数据类型,所有表都习惯性地以“info_”结尾。表用于描述层次关系的复合结构数据,整个Class文件实质上就是一张表。

其中类似于紧挨着的constant_pool_count、constant_pool 这样的数据可视为一个整体(一个表),前面记录后者数据的数量。

魔数与Class文件的版本

看class文件结构那张表,第一个就是u4 magic。这是一个占了4个字节的魔数,它的唯一作用就是确定这个文件是否为一个Class文件。它就是一个标志,告诉虚拟机自己是Class文件,这样做更加安全,四个字节储存的值是固定的,十六进制下为“0xCAFEBABE”,咖啡宝贝。

接下来分别是两个字节的minor(次版本)和两个字节的major(主版本)。分别储存着此Class文件时何种版本的编译器编译的,例如50.3,50就是主版本3就是次版本。在运行时可以向下兼容,比如51版本虚拟机可以运行50.3版本的class文件,但是反过来就不行了。

常量池

紧接着 constant_pool_count、constant_pool就是常量池部分。常量池可以理解为Class文件的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件最大的数据项目之一。

首先两字节的constant_pool_count是统计后面constant_pool的常量数量的。注意后面的数量是从1开始,例如constant_pool_count储存的数字是22,那么constant_pool中就储存了21个数据项。这么设计是为了让“第0个位置”储存写特殊的数据。Class文件只有这一部分计数是从1开始的,其他部分还是从0开始。

常量池中主要储存两大类常量:字面量和符号引用。字面量好理解就是注入字符串、final修饰的常量值等等。符号引用主要包含一下三个常量:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符

Class文件中不会储存各个方法、字段的最终内存分布,只有在执行到特定的代码时才会知道真正的内存入口(某信息的地址)。在JDK1.4中,常量池可包含的常量项如下(以后的版本会对内容进行扩充):

最麻烦的这些类型分别有自己的结构,不过共同的特点是第一个字节都储存着tag,即告诉虚拟机自己那种常量项。从这部分内容可以看出很多东西,比如说一个变量名称最大时两个字节,即64KB英文字符大小,当然按常理来说不会出现这样变态的变量名吧。

访问标志

在常量池结束之后,紧接着两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。

访问标志使用或来计算,比如一个类被ACC_PIBLIC(0x0001)、ACC_SUPER(0x0020)所修饰,那么计算为0x0001|0x0020 = 0x0021,该值就是被访问标志储存的值。Java中有专门计算关键字的包。

类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引是一组u2类型的数据的集合。Class文件中有这三项来确定继承关系。除了Object类以外,所有的夫索引都不是0。如果结构计数器的大小是0,那么后面那部分就没有数据。

字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量和实例级(对象级)变量,但不包括方法内部的局部变量。以下是字段表结构和字段表的第一个属性访问标志。

access_flags 的计算方式和前面类或者接口的访问表示相同。后面紧跟着两个属性是name_index

和 descriptor_index,分别代表着简易名称和方法描述符。

字段表集合中不会列出从超类或者父接口中继承下来的字段,但是可以列出本来Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性自动添加的字段。另外在Java中,同一个类不能出现简易名称相同的字段名,例如int name,后面紧跟着String name。但是在字节码层面,简易名称可以相同,后面的描述不同就好了。

方法表集合

方法表的结构和字段表的机构基本类似。

与字段表集合相对应,如果父类方法在子类中没有被重写,方法表集合中就不会出现父类的方法信息。在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求拥有一个与原方法不同的特征签名。特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,所以仅仅是返回值不同,不是重载。

属性表集合

在Class文件、字段表、方法表都携带自己的属性表集合。属性表的数据项目相对于其他部分比较宽松一点,但是内容也有很多。下面来看一下比较重要的。

Code属性

Java类的程序方法体中的代码经过编译后储存在Code属性中,但是接口和抽象类中的方法就不存在Code属性中。

max_locals代表了局部变量表所需要的储存空间,其中最小单位是Slot。其中Slot可以复用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,极大节省了空间。

code_length和code值储存的时Java源代码编译后生成的字节码指令。由于每个code只占了一个字节,所以能表示的指令数只有256个。code_length的长度虽然时四个字节,但是由于虚拟机的规定只能使用两个字节,所以最大只能编译65535条指令,一般来说也是够用了,但是在编译复杂的JSP的时候要注意,某些编译器会把JSP内容和页面输出的信息归并于一个方法中,就可能导致编译失败。

值得一提的是,Javac在编译方法的时候,参数即使你没有填,agrs_size也可能是1,这是由于隐式传进去了this,当然static修饰的方法参数就是0(不填写的情况下)。

曾经使用try-catch的时候,注意到finlly不会改变局部变量的值,以为是try已经return了,return之后才去执行的finlly中的数据,其实不然。例如下面这段代码。

public int inc(){
int x;
try{
x=1;
return x;
}catch(Exception e){
x=2;
return x;
}finally{
x=3;
}
}

这段代码永远不会输出x=3,执行顺序是这样的(以不会抛出异常为例):首先执行x=1,此时局部变量等于1.然后读到return指令,然后将x的值赋给一个空间,这个空间是return时返回的值,我们暂且将这块空间起个名字,叫做returnX,然后代码进入finally,注意此时,还在这个inc()方法的作用域中。然后将x赋值等于3,最后执行return指令,返回刚才那块returnX空间的值给调用者。离开inc()作用域,此时x那块Slot可以被复用。

其他

  1. Exceptions 储存方法throws后面的异常。
  2. LineNumberTable 不是必填项,但是默认填上,如果不填,抛异常栈的时候就无法定位到哪一行了。
  3. LocalVariableTable 不是必填项,用于描述栈帧中局部变量表中的变量于Java源码中定义的变量之间的关系。
  4. SourceFile 记录生成这个Class文件源码的名称
  5. ConstantValue 通知虚拟机自动为静态变量赋值,在初始化之前就进行赋值。
  6. InnerClasser 记录内部类和宿主类之间的关联。
  7. Signature 这个写AOP的时候经常见,此属性会为泛型记录信息,因为Java在编译的时候会进行泛型擦除,所以需要记录一下,让Java在运行的时候可以拿到泛型的原始信息。
  8. BootstrapMethods 这个属性保存invokedynamic指令引用的引导方法限定符,和Invoke包有很大关系。

字节码指令

字节码指令不会超过256个,一般来说一个指令后面会跟着参数,这很自然,就像我们写方法时需要加入参数(没有参数也是种参数)。但是由于Java虚拟机采用面向操作数栈而不是寄存器(编译语言)的架构,所以大多数情况下只包含一个操作码。

由于字节码数量有限,所以很多指令会被强制统一。比如处理boolean、byte、short和char类型的数组时,也会转化为对应的int类型的字节码指令来处理。

字节码操作的时候可能会导致溢出,例如两个很大的正整数相加,结果可能会称为一个负数。当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数字定义的话,将会使用NaN值来表示。所有使用NaN值作为操作数的算术操作,结果会返回NaN。

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都时使用管程(Monitor)来支持的。可以看作Synchronized此时拿的锁就是Monitor。

【Java杂货铺】JVM#Class类结构的更多相关文章

  1. java之jvm学习笔记十三(jvm基本结构)

    java之jvm学习笔记十三(jvm基本结构) 这一节,主要来学习jvm的基本结构,也就是概述.说是概述,内容很多,而且概念量也很大,不过关于概念方面,你不用担心,我完全有信心,让概念在你的脑子里变成 ...

  2. Java虚拟机JVM相关知识整理

    Java虚拟机JVM的作用: Java源文件(.java)通过编译器编译成.class文件,.class文件通过JVM中的解释器解释成特定机器上的机器代码,从而实现Java语言的跨平台. JVM的体系 ...

  3. JAVA总结--jvm

    VM,Virtual Machine 即虚拟机,指通过软件模拟的具有完整硬件系统功能的.运行在一个完全隔离环境中的完整计算机系统. JVM,Java Virtual Machine 即Java虚拟机, ...

  4. java内功 ---- jvm虚拟机原理总结,侧重于虚拟机类加载执行系统

    参考书籍:<深入理解java虚拟机>,三天时间用了八个小时看完,像读一本武侠小说,挺爽. 另外需声明:图片都是从我自己的csdn博客转载,所以虽然有csdn标识,但都是我自己画的图片. j ...

  5. Java虚拟机JVM学习07 类的卸载机制

    Java虚拟机JVM学习07 类的卸载机制 类的生命周期 当Sample类被加载.连接和初始化后,它的生命周期就开始了. 当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就 ...

  6. Java虚拟机JVM学习06 自定义类加载器 父委托机制和命名空间的再讨论

    Java虚拟机JVM学习06 自定义类加载器 父委托机制和命名空间的再讨论 创建用户自定义的类加载器 要创建用户自定义的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的f ...

  7. Java虚拟机JVM学习05 类加载器的父委托机制

    Java虚拟机JVM学习05 类加载器的父委托机制 类加载器 类加载器用来把类加载到Java虚拟机中. 类加载器的类型 有两种类型的类加载器: 1.JVM自带的加载器: 根类加载器(Bootstrap ...

  8. Java虚拟机JVM学习04 类的初始化

    Java虚拟机JVM学习04 类的初始化 类的初始化 在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值. 在程序中,静态变量的初始化有两种途径: 1.在静态变量的声明处进行初始 ...

  9. Java虚拟机JVM学习03 连接过程:验证、准备、解析

    Java虚拟机JVM学习03 连接过程:验证.准备.解析 类被加载后,就进入连接阶段. 连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去. 连接阶段三个步骤:验证.准备和解析. 类 ...

  10. Java虚拟机JVM学习02 类的加载概述

    Java虚拟机JVM学习02 类的加载概述 类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对 ...

随机推荐

  1. centos7+nginx+php+mysql环境搭建

    一:CentOS7安装 在VMware 新建一个虚拟机CentOS 64位,配置好磁盘大小为30G,内存2G,启动虚拟机进入CentOS安装界面 选择Install CentOS 7 SOFTWARE ...

  2. 一文说透 Spring 循环依赖问题

    https://zhuanlan.zhihu.com/p/62382615 循环依赖发生的时机 Bean 实例化主要分为三步,如图: 问题出现在:第一步和第二步的过程中,也就是填充属性 / 方法的过程 ...

  3. quartz详解3:quartz数据库集群-锁机制

    http://blog.itpub.NET/11627468/viewspace-1764753/ 一.quartz数据库锁 其中,QRTZ_LOCKS就是Quartz集群实现同步机制的行锁表,其表结 ...

  4. ETL优化

    ETL优化 Extract.Transform.Load,对异构数据源进行数据处理. 设立基线标准,根据硬盘.网络传输速度,多测测量得到数据量(m)/时间(s)的比值,找线性关系.建立基线作为调试和优 ...

  5. POJ 1159:Palindrome 最长公共子序列

    Palindrome Time Limit: 3000MS   Memory Limit: 65536K Total Submissions: 56273   Accepted: 19455 Desc ...

  6. BZOJ 3170 [Tjoi2013]松鼠聚会

    题解:切比雪夫距离转化为曼哈顿距离 枚举源点,横纵坐标互不影响,分开考虑,前缀和优化 横纵分开考虑是一种解题思路 #include<iostream> #include<cstdio ...

  7. Angular表单 (一)表单简介

    Angular 表单 angular提供了两种不同的方法来通过表单处理用户输入:响应式表单和模板驱动表单.二者都从视图中捕获用户输入事件.验证用户输入.创建表单模型.修改数据模型,并提供跟踪这些更改的 ...

  8. WOJ 1542 Countries 并查集转化新点+最短路

    http://acm.whu.edu.cn/land/problem/detail?problem_id=1542 今天做武大的网赛题,哎,还是不够努力啊,只搞出三个 这个题目一看就是个最短路,但是题 ...

  9. flask前后端数据交互

    1.后端如何得到前端数据1)如果前端提交的方法为POST:后端接收时要写methods=[‘GET’,‘POST’]xx=request.form.get(xx);xx=request.form[’‘ ...

  10. 3D打印前途光明,它需要怎样的进化?

    在很长一段时间内,笔者都认为3D打印只会存在于科幻场景内,众多的科技大佬在前几年也和我保持相当一致的看法,代工大王郭台铭曾口出狂言:如果3D打印能够普及,我就把"郭"字倒过来写,时 ...