一文深入理解 Java 虚拟机

点这里

类文件结构

class类文件的结构

任何一个Class文件都对应着唯一的一个类或接口的定义信息[插图],但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)。

《Java虚拟机规范》

根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。

无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。

字节码由10部分组成,依次是魔数、版本号、常量池、访问权限、类索引、父类索引、接口索引、字段表索引、方法、Attribute。

  1. 魔数  每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件

1个十六进制数对应 4 位 二进制数,那么CAFEBABE 一共 8 个十六进制数,一共需要 32 位二进制数,对应就是 4 个字节

  1. 版本号  由minor_version(次版本号)major_version(主版本号) 组成各占两个字节

  2. 常量池  Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

·被模块导出或者开放的包(Package)

类和接口的全限定名(Fully Qualified Name)

·字段的名称和描述符(Descriptor)

·方法的名称和描述符·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)

·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-ComputedConstant)

  1. 访问标识  紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等

  2. 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。

  1. 字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示

对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。

用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.StringtoString()的描述符为“()Ljava/lang/String;”,方法int indexOf(char[]source,intsourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,intfromIndex)的描述符为“([CII[CIII)I”。

     7.方法表集合  方法里的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。

有可能会出现由编译器自动添加的方法,最常见的便是类构造器“<clinit>()”方法和实例构造器“<init>()”方法

    8.属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

 字节码指令

Java字节码指令就是Java虚拟机能够听得懂、可执行的指令,可以说是Jvm层面的汇编语言,或者说是Java代码的最小执行单元。

javac命令会将Java源文件编译成字节码文件,即.class文件,其中就包含了大量的字节码指令。

Java虚拟机采用面向操作数栈而不是面向寄存器的架构(这两种架构的执行过程、区别和影响将在第8章中探讨),所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。

字节码指令分类:

  1. 存储和加载类指令:主要包括load系列指令、store系列指令和ldc、push系列指令,主要用于在局部变量表、操作数栈和常量池三者之间进行数据调度;(关于常量池前面没有特别讲解,这个也很简单,顾名思义,就是这个池子里放着各种常量,好比片场的道具库)

  2. 对象操作指令(创建与读写访问):比如我们刚刚的putfield和getfield就属于读写访问的指令,此外还有putstatic/getstatic,还有new系列指令,以及instanceof等指令。

  1. 操作数栈管理指令:如pop和dup,他们只对操作数栈进行操作。

  2. 类型转换指令和运算指令:如add/div/l2i等系列指令,实际上这类指令一般也只对操作数栈进行操作。

  1. 控制跳转指令:这类里包含常用的if系列指令以及goto类指令。

  2. 方法调用和返回指令:主要包括invoke系列指令和return系列指令。这类指令也意味这一个方法空间的开辟和结束,即invoke会唤醒一个新的java方法小宇宙(新的栈和局部变量表),而return则意味着这个宇宙的结束回收。

公有设计,私有实现

虚拟机实现的方式主要有以下两种:

·将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集;

·将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即即时编译器代码生成技术)。

精确定义的虚拟机行为和目标文件格式,不应当对虚拟机实现者的创造性产生太多的限制,Java虚拟机是被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的新的、有趣的解决方案。

class文件的变化

Class文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱。

Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础支柱之一。

虚拟机类加载机制

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

类的生命周期

java编译时不像其他语言需要连接,类型的加载、连接和初始化过程都是在程序运行期间完成的。编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过Java预置的或自定义类加载器,让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。运行时加载广泛应用于Java程序之中。

《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:·使用new关键字实例化对象的时候。·读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。·调用一个类型的静态方法的时候。

  2. 2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。

  1. 3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  2. 4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  1. 5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

  2. 6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

接口与类真正有所区别的是前面讲述的六种“有且仅有”需要触发初始化场景中的第三种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载过程

加载

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

获取类的二进制字节流的形式

  1. ·从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。

  2. ·从网络中获取,这种场景最典型的应用就是Web Applet。·运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。

  1. ·由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。·从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

  2. ·可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探。

验证

验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

“停机问题”(Halting Problem)[插图],即不能通过程序准确地检查出程序是否能在有限的时间之内结束运行。

准备

正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。表7-1列出了Java中所有基本数据类型的零值。

解析

Java虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关

1.类或接口的解析

需要判断该类是否是数组类型

如果我们说一个D拥有C的访问权限,那就意味着以下3条规则中至少有其中一条成立:

·被访问类C是public的,并且与访问类D处于同一个模块。

·被访问类C是public的,不与访问类D处于同一个模块,但是被访问类C的模块允许被访问类D的模块进行访问。

·被访问类C不是public的,但是它与访问类D处于同一个包中。

2.字段解析

首先将会对字段表内class_index[插图]项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用;

3.方法解析

先解析出方法表的class_index[插图]项中索引的方法所属的类或接口的符号引用,如果解析成功,那么我们依然用C表示这个类。

1)由于Class文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现class_index中索引的C是个接口的话,那就直接抛出java.lang.IncompatibleClassChangeError异常。

2)如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4)否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError异常。

5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。

4.接口方法解析

方法解析类似

JDK 9中增加了接口的静态私有方法,也有了模块化的访问约束,所以从JDK 9起,接口方法的访问也完全有可能因访问权限控制而出现java.lang.IllegalAccessError异常。

初始化

初始化阶段就是执行类构造器<clinit>()方法的过程。

·<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

<clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object。·由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

类加载器

对于任意一

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于Java中泛型类型的参数传入,我们需要先了解一下Java泛型的基本概念。 Java中的泛型是一种参数化类型的概念,即在定义类、接口或方法时,使用一个或多个类型参数来表示其中的某些类型,这些类型参数在使用时再被具体化。通过使用泛型,可以使代码更加通用、安全和可读性更强。 Java中的泛型类型参数可以用于类、接口和方法的定义中。在使用时,需要将具体的类型参数传递给它们,以指定其中的泛型类型。 下面以一个简单的例子来说明Java中参数传入泛型类型的用法。 ``` public class Box<T> { private T data; public Box(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } } ``` 在这个例子中,我们定义了一个泛型类Box,其中的类型参数T可以在类的定义中被指定。在Box类的构造函数和getData、setData方法中,我们使用了泛型类型T来表示其中的某些类型。 现在我们可以创建一个Box对象,并将一个具体的类型参数传递给它,以指定其中的泛型类型。例如: ``` Box<Integer> box = new Box<Integer>(new Integer(10)); ``` 在这个例子中,我们创建了一个Box对象,并将Integer类型作为泛型类型参数传递给它。这样一来,我们就可以在Box对象中存储和获取Integer类型的数据了。 同样地,我们也可以创建其他类型的Box对象,例如: ``` Box<String> box = new Box<String>("Hello World!"); Box<Double> box = new Box<Double>(new Double(3.14)); ``` 通过这种方式,我们可以方便地定义、使用和重用泛型类型,从而使代码更加通用和灵活。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值