JVM运行时数据区域以及javap的使用

JVM内存结构图

在这里插入图片描述

程序计数器

  • 程序计数器(Program counter Register,也叫PC寄存器)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过该改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 如果线程正在执行一个Java方法,程序计数器是有值的,且记录的是正在执行的字节码指令的地址。如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个没有规定任何OutOfMemoryError的区域。

Java虚拟机栈

  • Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同到都会创建一个栈帧(Stack Frame)用于存储局部量表、操作数栈、动态链接、方法出口等信息。栈帧是Java方法运行时的基础数据结构,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟栈中从入栈到出栈的过程。在JVM里面,栈帧的操作只有两种:出栈和入栈。正在被线程执行的方法称为当前线程方法,而该方法的栈帧就称为当前帧。
  • 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、long、float、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象始地址的引用指针,也可能是指向一个代表对象的句柄或其地与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
  • 在Java虚拟机规范中,对这个区域定了两种异状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;一般的虚拟机栈都是可扩展的,如果扩展时无法丰请到足够的内存,就会抛出OutOfMemoryError异常,可以通过-Xss设置每个线程的堆栈大小。
    在这里插入图片描述

局部变量表

  • 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时就在方法的code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

  • 局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short,int,float、reference或returnAddress类型的数据,这8种数类都可以使用32位或更小的物理存来存放,但这种描述与明确指出 “每个Slot占用32位长度的内存空间” 是有一些差别的,它运行Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。只要保证使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位拟机中的一致。

  • 一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、float、reference和returnAddress8种类型。第7种reference类表示对一个对象实例的引用,虚拟机规范既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但是一般来说,虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用直接或间接地查找到对象在Java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。第8种即returnAddress类型目前已经很少见了,现在已经由异常表代替。

  • 对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的引Slot空间。Java语言中明确的(reference类型则可能是32位也可能是64位),64位的数据类型只有long和double两种。虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量,索引 n 就代表了使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个,Java虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
    在这里插入图片描述

  • 为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。

  • 局部变量不像的类成员变量那样存在"准备阶段"。我们知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样的默认值。

操作数栈

  • 操作数栈(Operand Stack)也常称为操作栈,它是一个后人先出(Last In First out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写人到code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在maxstacks数据项中设定的最大值。

  • 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈\入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。

  • Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

动态连接

  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

  • Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

方法的返回地址

  • 当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用throw字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

  • 无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压人调用者栈帧的操作数栈中,调整pc计数器的值以指向方法调用指令后面的一条指令等。

本地方法栈

  • 本地方法栈(Native Method Stack)与虚拟机栈非常相似,也是线程私有的,它们的区别不过是虚拟机栈执行的是Java方法(也就是字节码),而本地方法栈用到的是Native方法。与虚拟机战一样。本地方法栈区域也会出现StackOverFlowError和OutOfMemoryError异常。

方法区

  • 方法区(Method Area),是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等。虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non一Heap(非堆),目的就是要和堆分开。
  • 对于Hotspot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)",但严格本质上说两者不同,或者说使用永久代来实现方法区而己,永久代是方法区(相当于是一个接口interface)的一个实现,idkl.7的版本中,己经将原本放在永久代的字符串常量池移走。Jdk1.7中方法区是用永久代实现的,到1.8中是用元空间(MetaSpace)实现的,而元空间使用的是直接内存。

运行时常量池

  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生产的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

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

    1. 类和接口的全限定名(Fully Qualified Name)
    2. 字段的名称和描述符(Descriptor)
    3. 方法的名称和描述符
  • Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存人口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

  • Java语言不要求常量一定只有编译器才能产生,运行时也可能将新的常量放入池中,该特性用的比较多的就是String类的intern()方法。运行时常量池是方法区的一部分,在内存不够时,也会抛出OutOfMemoryError异常。

Java堆

  • Java堆是被收集管理的主要区域,因此很多时候也被称做"GC堆"(Garbage Collected Heap)。从内存回收角度来看,由于现在收集器基本都采用分代算法,所以堆中还以细分:新生代(Young/New)和老年代(Old/Tenure),新生代又可以划分为Eden(伊甸园)空间、survivor(幸存区,其又可以分为from survivor和to survivor,也就是S0和S1)空间等。从内存分配的角度来看,线程共享的Java堆中可划分出多个程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的是为了更好地回收内存,或更快地分配内存。

  • 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只逻辑上是连续的即可。.Java虚拟机中可以对堆进行扩展,可以通过-Xms 设置起始堆大小、通过-Xmx设置最大堆大小、通过-XX:NewSize设置新生代最小空间大小、通过 -XX:MaxNewSize设置新生代最大空间大小。如果在堆中没有完成实例分配,并且地也无法再扩展时,将会抛OutOfMemoryError异常。

JDK7和JDK8常量池的对比

在这里插入图片描述
在这里插入图片描述
Jdk1.6及之前:有永久代,常量池1.6在方法区
Jdk1.7:有永久代,但己经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后:无永久代,常量池1.8在元空间

直接内存

  • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中新加入了NlO(New Inpu/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
  • 显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。当各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),会导致动态扩展时出现OutOfMemoryError异常。

结合javap命令理解栈帧

javap是JDK提供的一个命令行工具,javap能对给定的class文件提供的字节代码进行反编译。通过它,可以对照源代码和字节码,从而了解很多编译器内部的工作,对更深入地理解如何提高程序执行的效率等问题有极大的帮助。

  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

为了方便演示,我写了一个demo代码,并且用javac命令生成了class文件

	public class Test {
 
	public static final Integer CONSTANT = 666;

	public static Integer num1 = 111;

	public Integer num2 = 222;
	
	public int compute(int num1, int num2) {
		return num1 + num2;
	}
	
	public static void main(String[] args) {
		Test test = new Test();
		test.compute(Test.num1, test.num2);
	}
}

javap将字节码文件反汇编然后生成得内容如下:

D:\jdk\bin>javap -c C:\Users\Administrator\Desktop\Test.class
Compiled from "Test.java"
public class Test {
  // 定义的三个变量
  public static final java.lang.Integer CONSTANT;
  public static java.lang.Integer num1;
  public java.lang.Integer num2;
  
    static {};
    Code:
       0: sipush        666          // 将一个短整型常量值(-32768~32767)666推送至栈顶
       3: invokestatic  #2           // 调用静态方法,自动装箱 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       6: putstatic     #9           // 为指定的类的静态域赋值,CONSTANT = 666
       9: bipush        111          // 将单字节的常量值(-128~127)111推送至栈顶
      11: invokestatic  #2          // 调用静态方法,自动装箱 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      14: putstatic     #6          // 为指定的类的静态域赋值,num1 = 111
      17: return
  
  // 默认的构造方法
  public Test();
    Code:
       0: aload_0				// 将第一个引用类型本地变量推送至栈顶
       1: invokespecial #1      // 调用超类构造方法,实例初始化方法,私有方法
       4: aload_0              // 将第一个引用类型本地变量推送至栈顶
       5: sipush        222   // 将一个短整型常量值(-32768~32767)推送至栈顶
       8: invokestatic  #2    // 调用静态方法 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      11: putfield      #3    // 为指定的类的实例域赋值 Field num2:Ljava/lang/Integer;
      14: return

  public int compute(int, int);
    Code:
       0: iload_1             // 将第二个int型本地变量推送至操作栈,局部变量表的第一个是this引用
       1: iload_2             // 将第三个int型本地变量推送至操作栈
       2: iadd                // 将栈顶两int型数值相加并将结果压入栈顶
       3: ireturn             // 从当前方法返回int

  public static void main(java.lang.String[]);
    Code:
       0: new           #4                  // 创建一个Test对象,并将其引用值压入栈顶
       3: dup                               // 复制栈顶数值并将复制值压入栈顶
       4: invokespecial #5                  // 调用超类构造方法,实例初始化方法,私有方法 Method "<init>":()
       7: astore_1                          // 将栈顶引用型数值存入第二个本地变量
       8: aload_1                           // 将第二个引用类型本地变量推送至栈顶
       9: getstatic     #6                  // 获取指定类的静态域,并将其值压入栈顶 Field num1:Ljava/lang/Integer;
      12: invokevirtual #7                  // 调用实例方法 Method java/lang/Integer.intValue:()
      15: aload_1                           // 将第二个引用类型本地变量推送至栈顶
      16: getfield      #3                  // 获取指定类的实例域,并将其值压入栈顶 Field num2:Ljava/lang/Integer;
      19: invokevirtual #7                  // 调用实例方法 Method java/lang/Integer.intValue:()I
      22: invokevirtual #8                  // 调用实例方法 Method compute:(II)I
      25: pop                               // 将栈顶数值弹出 (数值不能是long或double类型的)
      26: return
}

文章参考:
《深入理解Java虚拟机第三版》、深入理解Java虚拟机栈的栈帧

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值