在进入主题前,我们先了解一些相关的知识,方面后面对运行时数据区进行分类。
进程中,有很多数据是多线程之间共享的,线程在执行时,会先从主存中读取数据,然后复制一份到高速缓存中,当计算完后,再刷新到主存中。
我们只要找到独属于线程的资源,那么其他的资源都是线程共享的,线程运行的本质就是函数的执行,函数运行时的信息保存在栈帧中,栈帧中保存了函数的返回值、调用其它函数的参数、java方法、局部变量、操作数栈、动态链接、方法出口、寄存器信息等。除了这个栈帧里面的信息,其他信息都是共享的,据此,我们先画个图,运行时数据区(Runtime Data Area)大致可以分成以下几个区域
Java运行时数据区(Runtime Data Area)是指在Java程序执行期间,Java虚拟机所管理的诸多内存区域(分别用于存储不同的数据),如上图所示,包含了以下几个部分:
该区域是一个共享区,主要用于存储对象实例、数组.
Java堆是虚拟机管理的内存中最大的一块区域,jvm只有一个堆区,在虚拟机启动时创建,被所有线程共享,堆是gc管理堆主要区域。
jvm堆一般分为三部分: 新生代,老年代,永久代(元空间)
永久代java8已经被元空间取代。
用来存放新生的对象,占据堆的1/3空间
如果新创建的对象占用内存很大,则直接分配到老年代。(当老年代也满了装不下的时候,就会抛出OOM异常。)
老年代的对象比较稳定,所以MajorGC不会频繁执行。
在进行MajorGC前一般都先进行了一次MinorGC, 使得有新生代的对象晋身入老年代,导致空间不够用时才触发。 当无法找到足够大的连续空间分配给新创建的较大对象时 也会提前触发一次MajorGC进行垃圾回收腾出空间。
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
Class在被加载的时候被放入永久区域。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中。
永久代(元空间)和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
虚拟机栈:java方法、局部变量、操作数栈、动态链接、方法出口等。
本地方法栈:native方法
栈帧
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。简言之,栈帧就是利用EBP(栈帧指针,请注意不是ESP)寄存器访问局部变量、参数、函数返回地址等的手段。
关于本地方法栈和Java栈,在Java虚拟机规范中定义了两种异常。
线程的请求栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
虚拟机在扩展栈时无法申请到足够的内存时,将抛出OutOfMemoryError异常
每次函数调用都会生成对应的栈帧,从而占用一定的内存
线程共享的内存区域,主要用于存储类型信息、常量、静态变量、即时编译代码等
CPU在执行程序时,需要有一个地方存放下一条要被取走指令的位置,这是一个寄存器,cpu中只有一个程序计数器。
虚拟机字节码指令的地址或undefined
每个线程都有私有(独立)的程序计数器。
线程中的程序计数器可以理解为一段内存,用来保存当前线程执行到的位置,因为系统采用时间片轮转的方法,所以一个线程不可能一直占用CPU,只能执行规定时间,进行线程切换,这里就需要有一个私有的线程计数器,也就是本地计数器,来保存当前线程的执行到的位置,等到下一次再从这个位置继续执行。
在多线程场景下,CPU会出现缓存一致性问题,处理器重新排序问题,
为了解决这个问题,制定了计算机内存模型。(原子性、可见性、有序性)
即是Java语言对这个操作规范的遵循,
JMM规定了所有的变量都存储在主存中,每个线程都有自己的工作区,线程将使用到的变量从主存中复制一份到自己的工作区,线程对变量的所有操作(读取、赋值等)都必须在工作区,不同的线程也无法直接访问对方工作区,线程之间的消息传递都需要通过主存来完成。可以把这里主存类比成计算机内存模型中的主存,工作区类比成计算机内存模型中的高速缓存。