运行时数据区
程序计数器(线程私有)
定义:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号显示器。
该空间为各线程私有空间,为保证线程切换后可以恢复到正确的执行位置。
若执行的是Native方法,则该计数器值为空(Undefined)。
Java虚拟机栈(线程私有)
定义:描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用到执行完成的过程,就是其栈帧在虚拟机栈中从入栈到出栈过程。
运行时栈帧结构
在编译代码时,一个栈帧需要分配多少内存空间,不受运行时变量数据的影响,仅决定于虚拟机的具体实现。栈帧的内存需求都写入到方法表的Code属性之中。
在活动线程中,位于栈顶的栈帧才是是有效的,称为当前栈,与其关联的方法称为当前方法。虚拟机执行引擎只对当前栈进行有关的字节码指令。
栈信息:包括动态链接、方法返回地址和其他附加信息归为一类。
局部变量表
定义:是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。其最大容量由方法的Code属性的max_locals决定。
变量槽 Slot:局部变量表的容量最小单位,其长度可以随处理器、操作系统或虚拟机的不同而变化。
- 一个Slot因存储一个32位以内的数据类型(包括boolean、byte、char、short、int、float、reference和returnAddress 这8种数据类型)。
- 对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间(包括long和double两种、reference 类型可能是32位,也可能是64位)。由于是线程的私有数据,无论读写连续两个Slot的是否为原子操作,都不会引起安全问题。
虚拟机通过索引定位的方式使用局部变量表,其值的范围是0到局部变量表的最大Slot数量。若访问32位的变量,索引n就代表了使用了第n个Slot。若是访问64位数据类型的变量,则同时使用n和n+1两个Slot。对于存放64位变量的两个Slot,不允许采用任何方式单独访问其中的某一个。若虚拟机遇到进行这种操作的字节码序列,会在类加载的校验阶段抛出异常。
虚拟机使用局部变量表完成参数值到参数变量列表的传递过程。如果执行的是实例方法,则局部变量的第0位索引的Slot用于传递方法所属对象实例的引用,可以通过“this”关键字访问该参数。其余参数按照参数列表顺序排列,占用从1开始的Slot。
局部变量表种的Slot是可以重用的,若当前字节码的PC值超过了某个变量的作用域,该变量的Slot就可以交由其他变量使用。
注意:在特定情况下,Slot的复用会影响系统的垃圾收集行为。因为虽然代码已经离开了变量的作用域,但若此后并未有其他变量对其Slot进行复用。所以作为GC Roots一部分的局部变量表仍然保持着对他的关联,故而并没有回收其内存。
由于局部变量并没像类变量有“准备阶段”,所以局部变量定义后必须赋值才能运行,否则会导致类加载失败。
操作数栈
定义:是一个先入后出的栈,其最大深度由方法表的Code属性的max_stacks项决定。
当一个方法开始执行时,其操作栈是空的,执行过程中,会有各种字节码指令在操作栈中写入和提取内容。32位数据类型所占的栈容量为1、64位数据类型的容量为2。
操作栈中的元素的数据类型必须与字节码指令的序列严格匹配,在编译和类校验阶段都要确保这一点。
优化:大部分的虚拟机运行两个栈帧出现部分重叠,主要是下面栈帧的部分操作栈与上面栈帧的部分局部变量表重叠,以公用部分数据,减少额外的参数复制传递。
动态链接
定义:每个栈帧都包含一个指向运行时常量池中该栈帧所属的方法的引用,持有这个引用是为了支持调用过程中的动态链接。
方法返回地址
退出方法的方式
- 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令,将返回值传递给上层方法调用者。
- 异常完成出口:方法执行过程中遇到异常(无论是虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常),并且该异常并没有在方法体内得到处理(方法的异常表中没有匹配的异常处理器),就会导致退出。
栈帧中需要保存一些信息以帮助方法返回时,恢复它的上层方法的执行状态。
- 正常退出时,调用者的PC值可以作为返回地址。
- 异常退出时,返回地址由异常处理器确定,栈帧不保存该信息。
附加信息
虚拟机允许具体的虚拟机实现增加一些规范里没有的描述信息于栈帧之中,如调试的相关信息。
本地方法栈(线程私有)
作用与Java虚拟机栈作用类似,但专门为虚拟机用到的Native方法服务。虚拟机对其中的方法使用的语言、使用方式和数据结构没有强制规定,可自由实现。
Java堆
定义:是被所有线程共享的一块内存区域、在虚拟机启动时创建。其唯一目的是存放对象实例(几乎所有的对象)。
Java堆在物理上可以是不连续的,但是在逻辑上必须是连续的。其没有固定的大小,可以是拓展的。虚拟机可通过(-Xmx和-Xms控制),若堆中没有内存,且无法进行拓展时,会抛出OutOfMemoryError异常。
具体划分
新生代
定义:主要用于存放新生的对象,一般占据堆的1/3的空间,一般会频繁的触发Minor GC进行垃圾回收。
- Eden区:Java新对象的出生地(如果新建对象占用内存很大,则直接分配到老年代)。当Eden区的内存不够时会触发一次Minor GC。
- ServivorTo区:保留本次Minor GC过程中的幸存者。
- ServivorFrom区:上次GC的幸存者,作为这一次GC的被扫描者。
老年代
定义:主要存放应用程序中生命周期较长的内存对象。
老年代的对象比较稳定,所以Major GC不会频繁执行。
本地线程分配换成(TLAB)
线程共享的Java堆中可能划分出多个线程私有的分配缓冲区,用于给线程分配内存。
方法区(可称为“永久代”,但两者并不等价)
定义:是各个线程的共享内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
当方法区无法满足内存分配要求后,会抛出OutOfMemoryError异常。
注意: 在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
运行时常量池
定义:是方法区的一部分,Class文件中用于存放编译时期生成的各种字面量和符号引用称为常量池,这部分内容在类加载进入方法区后就进入运行时常量池中存放。
备注:除了保存Class文件中描述符号引用外,还可以将翻译出来的直接引用存储在运行时常量池中。
动态性:运行期间产生的新的常量也可以放入池中(String类的intern()方法)。
直接内存
并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
-
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制。
-
配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常。
直接内存(堆外内存)与堆内存比较
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
- 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。
直接内存使用场景
- 有很大的数据需要存储,它的生命周期很长
- 适合频繁的IO操作,例如网络并发场景