前言
由于JAVA程序是交由JVM执行的,所以我们所说的JAVA内存区域划分也是指的JVM内存区域划分,JAVA程序具体执行的过程如下图所示。首先Java源代码文件会被Java编译器编译为字节码文件,然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
一、运行时数据区组成
线程共享数据区:方法区、堆
线程隔离数据区:虚拟机栈、本地方法栈、堆、程序计数器
1、程序计数器(PC寄存器)
(1)JVM支持多个线程同时运行,每个线程拥有一个程序计数器,是线程私有的,用来存储指向下一条指令的地址。
(2)如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
(3)由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。也是内存区域中唯一一个没有规定任何OutOfMemoryError情况的区域。
2、虚拟机栈
(1)栈是由一系列帧(Frame)组成(因此Java栈也叫作帧栈),是线程私有的。
(2)帧是用来保存一个方法的局部变量、操作数栈(java没有寄存器,所有的参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等。
(3)当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。
(4)由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。
虚拟机栈特点:
(1)局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用。
(2)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
补充:
(1)栈的优点:存取速度比堆快,仅次于程序计数器。
(2)栈的缺点:存在栈中的数据太小,生存期是在编译期决定的,缺乏灵活性。
(3)StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度。
(4)OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存。
3、Java堆
(1)JAVA堆是内存区域中一块用来存放对象实例的区域(几乎所有的对象实例都在这里分配内存),此内存区域的唯一目的就是存放对象实例。
(2)JAVA堆(Java Heap)是java虚拟机所管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域。
Java堆的特点:
(1)Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(GC主要管理堆空间,对分代GC来说,堆也是分代的)
(2)Java堆可以分成新生代和老年代,新生代可分为To Space、From Space、Eden。
补充:
(1)堆的优点:运行期动态分配内存大小,自动进行垃圾回收。
(2)堆的缺点:效率相对较慢。
4、方法区
(1)方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
(2)在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。
运行时常量池:
(1)是Class文件中每个类或接口的常量池表,在运行期间的表示形式,通常包括:类的版本、字段、方法、接口等信息。
(2)当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
5、本地方法栈
(1)在JVM中用来支持native方法执行的栈就是本地方法栈。
(2)本地方法栈与Java栈的作用和原理非常相似,区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的 。
(3)在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
二、JVM内存区域细化
1、栈、堆、方法区交互关系
2、Java堆的结构
3、对象
3.1、对象的内存布局
对象在内存中存储的布局(这里以HotSpot虚拟机为例说明),分为:对象头、实例数据和对齐填充。
3.2、对象头包含两个部分
(1)Mark Word:用于存储对象自身的运行时数据,如:HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
(2)类型指针:对象指向它的类元数据的指针。
3.3、对象的访问定位
在JVM规范中只规定了reference类型是一个指向对象的的引用,但没有规定这个引用具体如何去定位,访问堆中对象的具体位置。因此对象的访问方式取决于JVM的具体实现,目前主流的有:使用句柄、使用指针两种方式。
(1)使用句柄
Java堆中会划分出一块内存来做句柄池,reference中存储句柄地址,句柄中存储对象的实例数据和类元数据的地址。
(2)使用指针
(3)各自优势:
句柄访问:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针访问:速度快,它节省了一次指针定位的时间开销,由于对象的访问在JAVA中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。