概述
运行时数据区是JVM专门分配的一块内存空间,类加载子系统将字节码文件加载完成后,关于类的元数据信息,以及在程序处理中产生的中间结果,都存放在运行时数据区。
执行引擎会从运行时数据区中提取数据,并翻译成CPU能够识别的二进制指令,通过CPU计算后得出的结果再存回运行时数据区,从而完成交互。

运行时数据区分为 “线程私有区” 与 “线程共享区”:
线程私有区:
程序计数器(PC寄存器):存储当前线程即将执行的字节码指令的行号。
程序计数器用来记录接下去需要执行的字节码指令行号。在执行native本地方法时程序计数器没有值。
虚拟机栈(stack):存储Java方法调用与执行过程中的数据。
这里提一下栈和堆的区别,栈是一种数据结构,是运行时单位,即栈解决程序的运行问题,如何执行,如何处理数据,而堆是存储单位,堆解决数据存储在哪,怎么放。只有极少数(方法内部的基本数据类型的局部变量与对象的引用地址(非对象本身))存储在栈中,对象实例本身存储在堆中。
每个线程在创建时都会同步创建一个虚拟机栈,所以栈是线程私有的,其内部保存一个个栈帧,每一个栈帧对应着一次java方法调用,栈帧在栈中先进后出,当前栈顶的栈帧(方法)处理完后出栈,随后处理上一个栈帧,这点与java方法调用顺序一致,方法A调用了方法B后,需要等B方法执行完毕,再回到A方法继续执行。
栈的生命周期与当前线程一致,栈不存在垃圾回收问题。
栈帧的内部结构:
① 局部变量表:主要存储方法参数、定义在方法内的局部变量,这些数据包括各类基本数据类型,以及对象的引用。局部变量表是线程私有数据,所以不存在线程安全问题。局部变量表的大小是在编译期间就定下的,方法运行期间不会改变局部变量表的大小。
② 操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
虚拟机栈:存储Java方法调用与执行过程中的数据。
上面所说的“虚拟机栈”存储调用与执行过程中的数据只是广义的解释,真正对于字节码指令进行处理的是操作数栈。
③ 动态链接:将符号引用转化为直接引用,对运行时常量区中具体方法的引用。
④ 返回地址:当一个方法开始执行后,只有两种方式退出这个方法。
一种方法是执行引擎遇到任意一个方法返回的字节码指令,具体有无返回值需要根据何种返回字节码指令来确定。
另外一种方法是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理(意味着方法体内没有编写处理异常的代码,同时在字节码文件中的异常表Exception Table中也不会有内容),导致方法退出,这种退出方式称为“异常调用完成”,异常退出不会给方法调用者产生任何的返回值。
方法正常退出或是异常退出都必须返回到该方法被调用的位置,程序才能继续执行,正常退出时,当前线程的程序计数器的值作为返回地址。异常退出时,返回地址需要通过异常表来确定。(但如果异常表内没内容,是根据什么来指定返回地址的,待补充……)
HotSpot虚拟机的栈深度是无法动态扩展的,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError。而在可动态扩展栈深度的虚拟机中,如果扩展时无法申请到足够内存空间,会报出OutOfMemeryError。
本地方法栈:存储系统本地方法的执行数据。
线程共享区:
一个进程中只有一个JVM实例,所以对于一个进程来说,堆和方法区,都是唯一的,是进程中的多个线程共享的,所以需要考虑线程安全问题。
堆区(heap):存储实例化的对象,在JVM启动时就被创建,是JVM管理的最大一块内存空间,虽然所有线程共享堆区,但堆中仍可以划分出多个线程私有的分配缓冲区(TLAB),以提升对象分配时的效率。堆区可固定大小,也可动态扩展,如果堆中没有内存再完成实例分配同时也无法再扩展时,会报出OutOfMemoryError。
“所有”的对象实例与数组都分配在堆上(最近看到一些教程改用了“几乎所有”的说法,这里暂时先依循《Java虚拟机规范》中定义的“所有”来理解)。
方法区:用于存储已被虚拟机加载的类型信息(包含类与父类、接口的全限定名,访问标志,字段信息,方法信息等),以及运行时常量区:是方法区的一部分,存储字节码文件中的常量池信息(字面量和符号引用)。相较于字节码文件中的静态的常量池,运行时常量区的一个重要特性:具备动态性(待补充)。
方法区的大小决定了能保存多少个类,如果加载了太多的类,也会报出OutOfMemeryError。
垃圾收集器:
在方法结束后,堆中的对象不会立刻马上被移除,仅仅在垃圾回收的时候才会被移除,堆是GC(Garbage Collection垃圾收集器)执行垃圾回收的重点区域。
新生代与老年代:根据生命周期可以将堆中的实例对象分为两类,一种创建与销毁都非常迅速,称之为新生代,还有一种生命周期非常长,在极端情况下可以与JVM生命周期一致,称之为老年代,新生代与老年代在堆中的空间占比为1 : 2,新生代中又分为Eden区与Survivor0区、Survivor1区,空间占比为8 : 1 : 1,这些空间占比一般不会去修改。

对象的分配过程:
所有的对象实例都是在Eden区被创建的,当Eden区空间不足以再分配新对象时,会触发第一次GC(Minor GC),此时没有被任何引用的对象会被销毁以腾出空间存放新对象。仍在引用的对象age+1并存入S0区中,Eden区清空。
当再次GC时,会将Eden区中仍在引用的对象age+1并存入S1区,S0区中的上一次GC的幸存者如果此时未被引用,则销毁,如仍然被引用,一并存入S1区,Eden区与S0区清空。
再次GC时,重复以上动作,幸存者对象在S0区与S1区来回调换,所以Survivor区也称为from区与to区(to区永远是空的那个S0或S1)。
幸存者区的GC是被动的,会随着Eden区无法存放新对象时触发的from区复制到to区前,对幸存者区中的未被引用对象进行回收,当有幸存者对象age达到阈值15时,存入老年代,老年代内的对象不会被Minor GC。
特殊情况下:
1. 创建对象-->Eden放不下-->Minor GC-->Eden空-->Eden还是放不下-->说明对象本身比Eden还大-->超大对象直接进老年代。
2. 已经存入Survivor区内的对象,除了age到达15阈值,没有其他情况会直接晋升到老年代,破例直接存入老年代的对象只可能是Eden与Survivor区都存放不下的新生对象。
3. 2. 老年代空间不足以存放晋升的幸存者或是上面的超大对象时-->Major GC-->放得下放,放不下OutOfMemeryError。

部分收集:
新生代收集(Minor GC):只是新生代的垃圾收集。
老年代收集(Major GC):只是老年代的垃圾收集。
整堆收集(Full GC):整个堆与方法区的垃圾收集,执行效率低下,会导致系统长时间停滞,减少Full GC是JVM优化的重点。
GC的触发时机:
Minor GC:Eden空间不足会触发Minor GC,因为大部分对象是在Eden区被回收的,所以Minor GC非常频繁,回收速度也较快。Minor GC时会引发STW(stop the world),暂停其他用户的线程,等GC结束,其他线程才恢复运行。
Major GC:老年代空间不足时,会触发Major GC。Major GC速度要比Minor GC慢10倍以上,Major GC后,老年代空间仍然不足,会产生OutOfMemeryError。
Full GC:调用System.gc(); 系统建议执行Full GC,但不必然执行。
老年代空间不足。
方法区空间不足。
每次晋升到老年代的对象平均大小 > 老年代剩余空间。
目前只有CMS收集器会有“单独收集老年代”的行为,其他老年代收集器都会采用Full GC来回收老年代,而CMS收集器并非JDK8默认的老年代收集器,并且CMS由于会产生大量内存碎片,降低用户线程CPU效率等缺点,在JDK14中被放弃,所以在通常情况下,Major GC和Full GC可以是等价。