1.3 JVM的内存区域
JVM的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)和直接内存,如图1-2所示。
图1-2
线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。在JVM内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存在与否和本地线程的启动和销毁对应。
线程共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁。
直接内存也叫作堆外内存,它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。JDK的NIO模块提供的基于Channel与Buffer的I/O操作方式就是基于堆外内存实现的,NIO模块通过调用Native函数库直接在操作系统上分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用对内存进行操作,Java进程可以通过堆外内存技术避免在Java堆和Native堆中来回复制数据带来的资源占用和性能消耗,因此堆外内存在高并发应用场景下被广泛使用(Netty、Flink、HBase、Hadoop都有用到堆外内存)。
1.3.1 程序计数器:线程私有,无内存溢出问题
程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指示器。每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空(Undefined)。
程序计数器属于“线程私有”的内存区域,它是唯一没有Out Of Memory(内存溢出)的区域。
1.3.2 虚拟机栈:线程私有,描述Java方法的执行过程
虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧(Stack Frame)中存储了局部变量表、操作数栈、动态链接、方法出口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)。
栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法内未被捕获的异常),都视为方法运行结束。图1-3展示了线程运行及栈帧变化的过程。线程1在CPU1上运行,线程2在CPU2上运行,在CPU资源不够时其他线程将处于等待状态(如图3-1中等待的线程N),等待获取CPU时间片。而在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态。
图1-3
1.3.3 本地方法区:线程私有
本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。
1.3.4 堆:也叫作运行时数据区,线程共享
在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域。由于现代JVM采用分代收集算法,因此Java堆从GC(Garbage Collection,垃圾回收)的角度还可以细分为:新生代、老年代和永久代。
1.3.5 方法区:线程共享
方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据,如图1-4所示。
图1-4
JVM把GC分代收集扩展至方法区,即使用Java堆的永久代来实现方法区,这样JVM的垃圾收集器就可以像管理Java堆一样管理这部分内存。永久带的内存回收主要针对常量池的回收和类的卸载,因此可回收的对象很少。
常量被存储在运行时常量池(Runtime Constant Pool)中,是方法区的一部分。静态变量也属于方法区的一部分。在类信息(Class文件)中不但保存了类的版本、字段、方法、接口等描述信息,还保存了常量信息。
在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行时常量池中。Java虚拟机对Class文件每一部分的格式都有明确的规定,只有符合JVM规范的Class文件才能通过虚拟机的检查,然后被装载、执行。