运行时数据区:就是JVM在运行期间会使用的到的内存,其中一些会随着虚拟机启动和结束而创建和销毁,另外一些则是与线程一一对应的,这些与线程对应的数据区会随着线程的开始和结束而创建和销毁。每个JVM启动对应一个Runtime实例,即运行时环境。
运行时数据区被划分为6个区域
-
线程私有区域:程序计数器PC、虚拟机栈VMS、本地方法栈
-
线程共享区域:堆Heap、元数据区Metaspace、直接内存
在Hotspot JVM中线程主要分为
- 虚拟机线程:此类线程的操作需要JVM达到安全点才会出现,此时堆才不会变化。这种线程的执行类型包括”stop-the-world”的垃圾收集、线程收集、线程挂机、偏向锁撤销。
- 周期任务线程:此类线程是时间周期事件的体现(比如中断),一般用于周期性操作的调度执行。
- GC线程:此类线程对在JVM里不同种类的垃圾收集行为提供了支持
- 信号调度线程:此类线程接收信号并发送给JVM,在内部通过调用适当的方法进行处理。
-
编译线程:此类线程在运行时会将字节码编译成到本地代码
1 PC寄存器
PC寄存器:也叫程序计数器,一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。PC寄存器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖PC寄存器来完成,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候当切换回来的时候,无法知道该线程执行到那条指令,只能通过PC寄存器得知应该执行哪条指令。 JVM的字节码解释器需要通过改变PC寄存器的值来明确要执行的下一条字节码指令是哪条。
为什么每个线程都有一个PC寄存器?
多线程是在一个特定的时间段内只会执行其中一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
2 虚拟机栈
由于Java跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。所以指令集小,编译器容易实现,但是性能下降,实现同样的功能需要更多的指令。
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。设置栈空间大小:可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack frame),对应着一次次的Java方法调用。Java虚拟机栈是线程私有的,生命周期和线程一致。Java虚拟机栈主管Java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。
保存方法的局部变量:如果局部变量是基本数据类型,则保存局部变量的值;如果局部变量是引用数据类型,则保存局部变量的引用。
3 本地方法栈
本地方法栈用于管理本地方法的调用,本地方法栈是线程私有的,可以将本地方法栈设置成固定或者动态扩展的内存大小。如果线程请求分配的栈容量超过本地方法方法栈允许的最大容量,Java虚拟机会抛出StackOverflowError异常;如果本地方法栈可以动态扩展,并且创建新的线程时没有足够的内存去创建对应的本地方法栈时,Java虚拟机会抛出OutOfMemoryError异常。
本地方法栈的具体做法是Native Method Stack中等级native方法,在Execution Engine执行时加载本地方法库。
并不是所有的JVM都支持本地方法,Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,可以无需实现本地方法栈。在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
4 堆空间
一个JVM实例对应一个Runtime对象,也就是一个进程,堆是Java内存管理的核心区域(进程唯一、线程共享)
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大的一块空间,堆内存大小可以设置为固定大小,也可以是扩展的。
- 所有线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB),几乎所有的对象实例都在堆中分配内存,栈帧中保存对象引用。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除,堆是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。但是GC不能频繁进行垃圾回收,会影响性能。
-
Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该被视为连续的。
5 方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。方法区在JVM启动的时候被创建,并且它的实际的物理内存和Java堆区一样都可以是不连续的。关闭JVM就会释放这个区域的内存。
方法区的大小也可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:Metaspace
在jdk7及以前,习惯上把方法区称为永久代。jdk8开始,使用元空间取代了永久代。本质上,方法区和永久代并不等价,仅是对hostspot而言的。元空间和永久代最大的区别:元空间不在JVM设置的内存中,而是使用本地内存。
6 直接内存
直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。直接内存是在Java堆外的、直接向系统申请的内存区间。
- 直接内存来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存。通常,访问直接内存的速度优于Java堆。即读写性能高。所以读写频繁的场合可能会考虑使用直接内存。
- Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。
- 直接内存在Java堆外,因此大小不会直接受限于-Xmx指定的最大堆内存,但是由于系统内存是有限的,Java堆和直接内存的总和依然会受限于操作系统能给出的最大内存。
- 直接内存的缺点:分配回收成本较高、不受JVM内存回收管理
- 直接内存大小可以通过MaxDirectMemorySize参数设置。如果不指定,默认与堆的最大值-Xmx参数值一致。