`》》》》》》》》》文章末尾免费领取 JVM 学习资料《《《《《《《《《
在深入学习java虚拟机之前,首先需要了解JVM的整体结构,作为整个JVM知识体系的引入,我们先从oracle官网来作为学习的切入点,从JDK8官方说明可以知悉,JVM组成结构主要包括以下几个部分
运行时数据区(Run-Time Data Areas)
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
1.PC寄存器
Java虛拟机可以支持多条线程同时执行,每一条Java虚拟机线程都有自己的pc寄存器。在任意时刻,一条Java虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法。如果这个方法不是native的,那pc寄存器就保存Java虚拟机正在执行的字节码指令的地址,如果该方法是native的,那pc寄存器的值是undefined。pc寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值。
2.Java虚拟机栈
每一条 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,这个栈与线程同时创建,用于存储栈帧。Java 虚拟机栈的作用与传统语言(例如C语言)中的栈非常类似,用于存储局部变量与一些尚未算好的结果。另外,它在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的出栈和入栈之外, Java 虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆中分配,Java 虚拟机栈所使用的内存不需要保证是连续的。
在( Java 虚拟机规范)第 1 版中, Java 虚拟机找也称为“ Java 栈”。
Java 虚拟机规范既允许Java虚拟机栈被实现成固定大小,也允许根据计算动态来扩展和收缩。如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
Java虚拟机实现应当提供给程序员或者最终用户调节虚拟机找初始容量的手段,对于可以动态扩展和收缩Java虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
Java虚拟机栈可能发生如下异常情况:
(1)、如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个 StackoverflowError异常。
(2)、如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutofMemoryError异常。
3.堆
在Java虚拟机中,堆(heap)是可供各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。
Java堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统(automatic storagemanagement system,也就是常说的garbage collector(垃圾收集器)所管理的各种对象,这些受管理的对象无需也无法显式地销毁。本规范中所描述的Java虚拟机并未假设采用何种具体技术去实现自动内存管理系统。虚拟机实现者可以根据系统的实际需要来选择自动内存管理技术。Java堆的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存不需要保证是连续的。
Java虚拟机实现应当提供给程序员或者最终用户调节Java堆初始容量的手段,对于可以动态扩展和收缩Java堆来说,则应当提供调节其最大、最小容量的手段。
Java堆可能发生如下异常情况:
如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutofMemoryError异常。
4.方法区
在 Java 虚拟机中,方法区(method area)是可供各个线程共享的运行时内存区域:方法区与传统语言中的编译代码存储区(storage area for compiled code )或者操作系统进程的正文段(text segment)的作用非常类似,它存储了每一个类的结构信息,例如,运行时常量池 (runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集与压缩。这个版本的Java虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。
Java虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。
方法区可能发生如下异常情况:
如果方法区的内存空间不能满足内存分配请求,那么 Java虚拟机将抛出一个OutofMemoryError异常。
5.运行时常量池
运行时常量池(runtime constant pool)是class文件中每一个类或接口的常量池表(constant pool table)的运行时表示形式,它包括了若干种不同的常量,从编译期可知的数值字面量到必须在运行期解析后才能获得的方法或字段引用。运行时常量池类似于传统语言中的符号表(symbol table),不过它存储数据的范围比通常意义上的符号表要更为广泛。每一个运行时常量池都在Java虚拟机的方法区中分配,在加载类和接到虚拟机后,就创建对应的运行时常量池。
在创建类和接口的运行时常量池时,可能会发生如下异常情况:
当创建类或接口时,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那么Java虚拟机将会抛出一个OutofMemoryError异常。
6.本地方法栈
Java虚拟机实现可能会使用到传统的栈(通常称为C stack)来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(native method stack)。当Java虚拟机使用其他语言(例如C语言)来实现指令集解释器时,也可以使用本地方法栈。如果Java虚拟机不支持native方法,或是本身不依赖传统栈,那么可以不提供本地方法栈,如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。
Java虚拟机规范允许本地方法栈实现成固定大小或者根据计算来动态扩展和收缩。如果采用固定大小的本地方法栈,那么每一个线程的本地方法栈容量可以在创建栈的时候独立选定。
Java虚拟机实现应当提供给程序员或者最终用户调节本地方法栈初始容量的手段,对于长度可动态变化的本地方法栈来说,则应当提供调节其最大、最小容量的手段。
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutofMemoryError异常。
栈帧
栈帧(frame)是用来存储数据和部分过程结果的数据结构,同时也用来处理动态链接(dynamic linking)、方法返回值和异常分派(dispatch exception)。栈帧随着方法调用而创建,随着方法结束而销毁一无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表(local variable)操作数栈(operand stack)和指向当前方法所属的类的运行时常量池的引用。
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息,例如,对程序调试提供支持的信息。
本地变量表和操作数栈的容量在编译期确定,并通过相关方法的code属性保存及提供给栈帧使用。因此,栈帧数据结构的大小仅仅取决于Java虚拟机的实现。实现者可以在调用方法时给它们分配内存。
在某条线程执行过程中的某个时间点上,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧称为当前栈帧(current frame),这个栈帧对应的方法称为当前方法(currentmethod),定义这个方法的类称作当前类(current class)。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的局部变量表和操作数栈所进行的操作。
如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。调用新的方法时,新的栈帧也会随之而创建,并且会随着程序控制权移交到新方法而成为新的当前栈帧。方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,然后,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
请特别注意,栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一个线程的栈帧。
1.局部变量表
每个栈帧内部都包含一组称为局部变量表的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类或接口的二进制表示之中,即通过方法的cod属性保存及提供给栈帧使用。
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference或returnAddress的数据。两个局部变量可以保存一个类型为long或double的数据。
局部变量使用索引来进行定位访问。首个局部变量的索引值为0。局部变量的索引值是个整数,它大于等于0,且小于局部变量表的长度。
long和double类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量中较小的索引值来定位。例如,将一个doub1e类型的值存储在索引值为n的局部变量中,实际上的意思是索引值为n和+1的两个局部变量都用来存储这个值。然而,索引值为+1的局部变量是无法直接读取的,但是可能会被写入。不过,如果进行了这种操作,那将会导致局部变量n的内容失效。
前面提及的局部变量索引值n并不要求一定是偶数,Java虚拟机也不要求double和long类型数据采用64位对齐的方式连续地存储在局部变量表中。虚拟机实现者可以自由地选择适当的方式,通过两个局部变量来存储一个double或long类型的值。
Java虚拟机使用局部变量表来完成方法调用时的参数传递。当调用类方法时,它的参数将会依次传递到局部变量表中从0开始的连续位置上。当调用实例方法时,第0个局部变量一定用来存储该实例方法所在对象的引用(即Java语言中的this关键字)。后续的其他参数将会传递至局部变量表中从1开始的连续位置上。
2.操作数栈
每个栈帧内部都包含一个称为操作数栈的后进先出(Last-In-First-Out,LIFO)栈。栈帧中操作数栈的最大深度由编译期决定,并且通过方法的code属性保存及提供给栈帧使用。
在上下文明确不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操作数栈”。
栈帧在刚刚创建时,操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把操作结果重新入栈。在调用方法时,操作数栈也用来准备调用方法的参数以及接收方法返回结果。
例如,iadd字节码指令的作用是将两个int类型的数值相加,它要求在执行之前操作数栈的栈顶已经存在两个由前面的其他指令所放入的it类型数值。在执行iadd指令时,两个int类型数值从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算(subcomputation)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。
操作数栈的每个位置上可以保存一个Java虚拟机中定义的任意数据类型的值,包括long和double类型。
在操作数栈中的数据必须正确地操作。例如,不可以入栈两个it类型的数据,然后当做long类型去操作,或者入栈两个float类型的数据,然后使用iadd指令对它们求和。有一小部分Java虚拟机指令(例如dup和swap指令)可以不关注操作数的具体数据类型,把所有在运行时数据区中的数据当做裸类型(raw type)数据来操作,这些指令不可以用来修改数据,也不可以拆散那些原本不可拆分的数据,这些操作的正确性将会通过class文件的校验过程来强制保障。
在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位的栈深度。
3.动态链接
每个栈帧内部都包含一个指向当前方法所在类型的运行时常量池的引用,以便对当前方法的代码实现动态链接。在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用(symbolic reference)来表示,动态链接的作用就是将这些以符号引用所表示的方法转换为对实际方法的直接引用。类加载的过程中将要解析尚未被解析的符号引用,并且将对变量的访问转化为变量在程度运行时,位于存储结构中的正确偏移量。
由于对其他类中的方法和变量进行了晚期绑定(late binding)所以即便那些类发生变化,也不会影响调用它们的方法。
4.方法调用正常完成
方法调用正常完成是指在方法的执行过程中,没有抛出任何异常——包括直接从Java虚拟机中抛出的异常以及在执行时通过hrow语句显式抛出的异常。如果当前方法调用正常完成,它很可能会返回一个值给调用它的方法。方法正常完成发生在一个方法执行过程中遇到了方法返回的字节码指令时,使用哪种返回指令取决于方法返回值的数据类型(如果有返回值)。
在这种场景下,当前栈帧承担着恢复调用者状态的责任,包括恢复调用者的局部变量表和操作数栈,以及正确递增程序计数器,以跳过刚才执行的方法调用指令等。调用者的代码在被调用方法的返回值压入调用者栈帧的操作数栈后,会继续正常执行。
5.方法调用异常完成
方法调用异常完成是指在方法的执行过程中,某些指令导致了Java虚拟机抛出异常,并且虚拟机抛出的异常在该方法中没有办法处理,或者在执行过程中遇到throw字节码指令并显式地抛出异常,同时在该方法内部没有捕获异常。如果方法异常调用完成,那一定不会有方法返回值返回给其调用者。
微信搜索 “Java 码仔”,回复 “jvm” 免费领取 JVM 学习资料
【本文来源:美国服务器 https://www.68idc.cn 复制请保留原URL】