运行时数据区域简介

Java虚拟机在执行Java程序的时候会把它管理的内存划分成为若干个不同的数据区域,这些区域就叫做运行时数据区域,不同的数据区域有着不同的用途、不同的创建及销毁时间。运行时数据区域主要包括:方法区、虚拟机栈、本地方法栈、堆、程序计数器,下面这张图就是大致的运行时数据区域:
运行时数据区域 - 图2

线程私有运行时数据区域

程序计数器

程序计数器是一块较小的内存空间,它可以看成是当前线程执行字节码的行号指示器。当前线程需要通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个程序计数器来完成。

如果线程正在执行的是一个Java方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是本地方法,那么这个计数器则为空。也就是说,这个计数器仅仅只是记录Java代码的字节码指令的执行地址。该内存区域也是唯一一个在《Java虚拟机规范》中没有规定任何OOM的区域。

Java虚拟机栈

Java虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储:局部变量表、操作数栈、动态链接、方法出口。每一个方法被调用直至执行完毕的过程就是对应一个栈帧在虚拟机栈从入栈到出栈的过程。
image.png

介绍一下比较重要的局部变量表:Java虚拟机栈中的局部变量表存放了编译期可知的Java虚拟机基本数据类型(boolean、byte、char、short、float、long、double)和对象引用(reference类型)。这些数据类型在局部变量表中的存储空间是以局部变量槽(Slot)来表示的,其中64位长度的long和double类型的数据会占用两个变量槽,其余类型的数据占用一个。局部变量表的空间大小是在编译时分配的,当进入一个方法,对应的栈帧中分配多大内存的局部变量表是完全确定的,方法执行期间并不会改变局部变量表的大小。

注意: reference类型并不等同于对象本身,《Java虚拟机规范中》并没有规定它具体是什么,它可以是一个指向对象起始地址的指针,也可以是一个代表对象的句柄或者其它与引用的对象相关的位置。

本地方法栈

本地方法栈和Java虚拟机栈的作用是非常相似的,只不过服务的方法不是Java方法,而是本地(native)方法,本地方法的底层一般是C/C++,因此这里就不详细介绍本地方法栈了。
image.png

线程共享运行时数据区域

Java堆

Java堆是虚拟机所管理的内存最大的一块,它是被所有线程共享的一块内存区域,在虚拟机启动时创建。《Java虚拟机规范中》是这样描述Java堆的:“所有的对象实例以及数组都应该在堆上分配”,由此可知几乎所有的实例对象都存放在Java堆中。在有些虚拟机中,Java堆还可以划分出多个线程私有的分配缓冲区,这样的目的只是为了更好的分配或者回收内存以提高效率,但是无论如何都不会改变堆的存储内容的共性。当前主流的虚拟机中的Java堆的大小是可以设置的,一般通过参数“-Xmx”合“-Xms”来设定。而如果当Java堆中已经没有空间分配给新的实例对象并且堆已经不能再扩展了,那么就会抛出OOM。
image.png

注意:

  1. JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
  2. 存储的是对象,每个对象都包含一个与之对应的class
  3. 对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定

image.png

方法区

《Java虚拟机规范》中对方法区的描述:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择区进行垃圾收集或者进行压缩。”对于HotSpot虚拟机来说,方法区还有一个叫做“非堆”的别名,目的是与堆区分开来,所有通常方法区看作是独立于堆的一块内存区域。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区的溢出,虚拟机同样会抛出内存溢出错误:OOM。方法区主要用于存储已被虚拟机加载的类型信息、常量、静态变量、JIT代码缓存、域信息、方法信息。
image.png

类型信息

对每个加载的类型(类 class、接口 interface、枚举enum、注解annotation),JVM的方法区中存储以下类型信息:

  • 这个类型的全限定名
  • 这个类型直接父类的全限定名
  • 这个类型的修饰符( { public,abstract ,final } 的某个子集)
  • 这个类型直接接口的一个有序列表

    运行时常量池

    在介绍运行时常量池之前,首先需要先介绍常量池和字符串常量池。

    class常量池

    常量池(Constant Pool),也叫 class 常量池(Class Constant Pool)。java文件被编译成 class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool),用于存放编译器生成的各种字面量( Literal )和 符号引用(Symbolic References)。如图:
    image.png
    其中字面量( Literal )就是我们所说的常量概念,如:

  • 文本字符串

  • 被声明为final的常量值
  • 基本数据类型的值

符号引用Symbolic References) 是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用一般包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(如下所示),代表当前这个常量属于哪种常量类型。

序号 常量池中数据项类型 类型标志 类型描述
1 CONSTANT_Utf8 1 UTF-8 编码的Unicode字符串
2 CONSTANT_Integer 3 int 类型字面值
3 CONSTANT_Float 4 float 类型字面值
4 CONSTANT_Long 5 long 类型字面值
5 CONSTANT_Double 6 double 类型字面值
6 CONSTANT_Class 7 对一个类或接口的符号引用
7 CONSTANT_String 8 String 类型字面值
8 CONSTANT_Fieldref 9 对一个字段的符号引用
9 CONSTANT_Methodref 10 对一个类中声明的方法的符号引用
10 CONSTANT_InterfaceMethodref 11 对一个接口中声明的方法的符号引用
11 CONSTANT_NameAndType 12 对一个字段 或 方法的部分符号引用

每种不同类型的常量类型具有不同的结构,具体的可以查看《深入理解java虚拟机》第六章的内容。

字符串常量池

String Pool(字符串池),也即String Literal Pool,又叫全局字符串池、字符串常量池。是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到 String Pool 中。在 HotSpot虚拟机中实现的 String Pool 功能的是一个 StringTable 类,它是一个哈希表,里面存的是 驻留字符( 也就是用双引号括起来的部分)的 引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个 StringTable 引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个 HotSpot VM 的实例只有一份,被所有的类共享。

运行时常量池

java文件被编译成class文件之后,也就是会生成我上面所说的 class常量池。jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。

每个class文件都有一个常量池,存放着字符串常量,类和接口名字,字段名以及其它一些在class中引用的常量。但是只有class文件中的常量池肯定是不够的,因为我们的代码需要在JVM中运行起来,这时候就需要一个运行时常量池,为JVM的运行进行服务。运行时常量池和class文件的常量池是一一对应的,它就是class文件的常量池来构建的。而当类加载到内存中后,jvm就会将 class常量池 中的内容存放到 运行时常量池 中,由此可知,运行时常量池 也是每个类都有一个。class常量池和运行中常量池都是方法区的组成:
image.png
注意上图,class文章信息并不等同于class文件。

参考文章: https://blog.csdn.net/xiaojin21cen/article/details/105300521

几种变量的区别

Java中,变量一般分为成员变量和局部变量。成员变量作用范围是整个类,相当于C语言中的全局变量,定义在 方法体 和 语句块 之外,一般定义在类的声明之下;而局部变量作用范围在它定义的方法体或者语句块内部,出了这个范围就无效了。下面是几种易混的成员变量:
image.png

注意: 上面提到的成员变量是不受权限修饰符(如:private)限制的

对于局部变量,需要注意几点:

  • 访问修饰符(如:private)不能用于局部变量
  • 局部变量声明在方法、构造方法或者语句块中
  • 局部变量在方法、构造方法、或语句块被执行的时候创建,当它们执行完成后变量就会被销毁
  • 局部变量只在声明它的方法、构造方法或者语句块中可见
  • 局部变量是存放在栈里面的
  • 局部变量没有默认值,所以局部变量被声明后,必须经过初始化才可以使用

    参考文章: https://blog.csdn.net/xiaojin21cen/article/details/87689384

静态变量

静态变量和类关联在一起,随着类的加载而加载,成为类数据在逻辑上的一部分。

JIT代码缓存

这个了解即可:JIT 是 just in time 的缩写, 也就是即时编译编译器。在运行时 JIT 会把翻译过的机器码保存起来,以备下次使用,因此从理论上来说,采用该 JIT 技术可以接近以前纯编译技术。
image.png

域(Field)信息

JVM 必须在方法区中保存类的所有域的相关信息以及域的声明顺序,域信息包括:

  • 域名称
  • 域类型
  • 域修饰符( { public,private,protected,static,final,volatile,transient } 的某个子集)

    方法(Method)信息

    JVM 必须在方法区中保存类的所有方法的相关信息以及方法的声明顺序,域信息包括:

  • 方法名称

  • 方法类型
  • 方法饰符( { public ,private, protected , static ,final, synchronized, native,abstract } 的某个子集)
  • 方法参数的数量和类型(按顺序存储)
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native方法除外)
  • 异常表 (abstract 和 native 方法除外):每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

image.png

直接内存

直接内存并不是jvm运行时数据区,也不是属于jvm管理的内存区域,而是操作系统直接管理一块内存区域。这部分内存也被频繁的使用,而且也是有可能导致OOM异常出现的,但是直接内存的分配不会受到jvm的影响,而是本机总内存的影响。JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数直接分配堆外内存,然后通过存储在Java堆里面的DirectBytrBuffer对象作为这块内存的引用进行操作,这样能够显著提高性能,避免在Java堆和Native堆中来回复制数据。