# 运行时数据区

image-20210329223428131

  • 程序计数器(Program Counter Register)
  • Java虚拟机栈(Java Virtual Machine Stack)
  • 本地方法栈(Native Method Stacks)
  • Java堆(Java Heap)
  • 方法区(Method Area)
  • 运行时常量池(Runtime Constant Pool)
  • 直接内存(Direct Memory)

# 程序计数器(Program Counter Register)

可看作当前线程执行字节码的行号指示器,字节码解释器工作时就是改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支/循环/跳转/异常处理/线程恢复都依赖它

任何一个确定时刻,一个逻辑处理器都只会执行一条线程中的指令,所以,为了在线程切换后能恢复到正确的执行位置,每条线程都有自己独立的程序计数器,互相隔离

如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令地址,如果是本地Native方法,计数器值为空(Undefined)

  • 存放线程当前执行的虚拟机字节码指令地址
  • 线程隔离
  • 无OutOfMemoryError异常

# Java虚拟机栈(Java Virtual Machine Stack)

image-20210329222720972

每个方法被线程执行的时候,Java虚拟机栈会同步创建一个栈帧(Stack Frame),用于存储局部变量表/操作数栈/动态连接/方法出口等信息,每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从出栈到入栈的过程(一个线程为一个栈,一个栈由多个栈桢组成,一个栈桢对应一个方法,一个方法有多个安全点)

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean/byte/char/short/int/float/long/double),对象引用(reference类型,指向一个对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置),returnAddress类型(指向一条字节码指令的地址)

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double会占用两个变量槽,其余类型只占一个。局部变量表所需的内存空间在编译器完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小,这里的大小指的是变量槽的数量,虚拟机真正使用多大内存空间来实现一个变量槽,是完全由具体的虚拟机实现自行决定的

  • 存放局部变量表/操作数栈/动态连接/方法出口,栈帧随着方法执行而出栈入栈
  • 线程隔离
  • 线程请求栈深度大于虚拟机所允许深度,抛StackOverflowError异常
  • 栈可扩展且扩展时无法申请到足够内存,抛OutOfMemoryError异常

# 本地方法栈(Native Method Stacks)

本地方法栈与Java虚拟机栈非常相似,区别只是前者执行Java方法(也就是字节码)服务,后者为执行本地方法服务

  • 线程共享

  • 线程请求栈深度大于虚拟机所允许深度,抛StackOverflowError异常

  • 栈可扩展且扩展时无法申请到足够内存,抛OutOfMemoryError异常

# Java堆(Java Heap)

垃圾收集管理的内存区域,几乎所有的对象实例都在这里分配内存,由于现在垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现新生代/老年代/永久代/Eden空间/From Survivor空间/To Survivor空间等名词

  • 存放对象实例
  • 垃圾收集算法的工作区域
  • 线程共享
  • Java堆中无法完成实例分配,且也无法再拓展时,抛OutOfMemoryError异常

# 方法区(Method Area)

别名非堆(Non-Heap),用于存储已被虚拟机加载的类型信息/常量/静态变量/即时编译器编译后的代码缓存等数据

JDK8以前,方法区经常被称呼为永久代,或者将两者混为一谈,本质上并不等价,只是HotSpot虚拟机设计团队选择将收集器的分代设计拓展至方法区,或者说使用了永久代来实现了方法区,这样便可以让垃圾收集器像管理Java堆一样管理这部分内存,省去专门编写内存管理代码的工作。回头来看,这并不是一个好主意,这种设计导致了Java应用更容易遇到内存溢出问题,所以在JDK6的时候,HotSpot开发团队就开始逐渐放弃永久代,逐步改为采用本地内存来实现方法区了,到了JDK7,已经把原本放在永久代的字符串常量池/静态变量等移出,到了JDK8时,终于完全废弃了永久代的概念,改用本地内存实现的元空间(Metaspace)来代替,把JDK7中剩余的内容,主要是类型信息,全部移到元空间中

  • 别名非堆(Non-Heap)
  • 存放已被虚拟机加载的类型信息/常量/静态变量/即时编译器编译后的代码缓存等数据
  • JDK6-JDK8逐步使用本地内存实现的元空间(Metaspace)来代替原先在Java堆中的永久代
  • 无法满足新的内存分配需求时,抛OutOfMemoryError异常

# 运行时常量池(Runtime Constant Pool)

是方法区的一部分,Class文件中除了有类的版本/字段/方法/接口等描述信息外,还有常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

  • 方法区的一部分
  • 存放编译期生成的各种字面量与符号引用
  • 当常量池无法再申请到内存时,抛OutOfMemoryError异常

# 直接内存(Direct Memory)

并不是虚拟机运行时数据区的一部分,但是也可能导致OutOfMemoryError异常

JDK1.4中加入了NIO(New Input/Output),引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据

  • 当物理内存不足以满足内存使用时,抛OutOfMemoryError异常

# 总结

  • 为了存放一些生命周期较长(与Java进程同生共死)的元素,且无需动态回收,比如已被虚拟机加载的类型信息/常量/静态变量/即时编译器编译后的代码缓存等,更倾向于放在虚拟机运行时数据区外的元空间中,那么生命周期较短的元素,就倾向于放在虚拟机运行时数据区内,方便管理内存空间的分配与释放
  • 那么在虚拟机运行时数据区内的元素,经过多次垃圾收集仍然存活的对象实例,便会被分配到老年代,并以较低频率被GC,而朝生夕死的临时对象实例,则会被分配到新生代,以较高频率被GC
  • 而像是生命周期再短些的,如局部变量表/操作数栈/动态连接/方法出口等,基本上是随着方法的执行开始到完成就走过了一生的,会被存放在Java虚拟机栈中,随着栈帧的出栈入栈而存活消亡
最近更新: 7/5/2021, 8:38:17 AM