java虚拟机在运行时会将内存空间划分为不同的数据区域。每个区域都有各自的用途以及生命周期。有些区域伴随着JVM进程的存在而存在,有些区域“随线程而生,随线程而死”。
JVM运行时数据区
程序计数器
作用:当前线程所执行的字节码的行号指示器,字节码解释器在工作时通过改变计数器的值来选取下一条需要执行的字节码指令。
特点:
1、线程私有,JVM多线程是通过轮流切换并分配CPU时间的,在任何一个时刻,一个CPU(一个核)只会执行一条线程中的指令。为了保证每条线程切换后恢复到正确的位置,每个线程都需要一个独立的程序计数器。
2、如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的时native方法,则计数器的值为空。
3、此内存区域是唯一一个没有规定任何OOM情况的区域。
虚拟机栈
虚拟栈描述的是java方法执行时的内存模型(JMM),每个方法在执行的时候会创建一个栈帧(stack frame),栈帧的数据结构包括局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从开始执行到退出的过程,及对应着栈帧在虚拟机栈中入栈到出栈的过程。(关于栈帧及方法执行的具体细节,在执行子系统中会有具体描述)
虚拟机栈可能出现的异常状况:
stackOverFlow:栈溢出,如果方法调用的栈深度大于虚拟机允许的最大栈深度,就会出现stackOverFlow异常。递归方法调用时,如果调用栈过深,就有可能引发栈溢出。这也是为什么慎用递归的原因之一以及编写递归时的注意点。
OutOfMemory:如果虚拟机栈允许动态扩展,在扩展时无法申请到足够的内存,就会抛出OOM异常。
本地方法栈
与虚拟机栈作用类似,区别是虚拟机栈为执行字节码服务,而本地方法栈为虚拟机使用到的native方法服务。由于虚拟机规范中没有对本地方法栈中使用的语言、数据结构做出强制规定,具体的虚拟机可以自由实现。Sun HotSpot虚拟机直接把虚拟机栈和本地方法栈合二为一。本地方法栈也会抛出stackOverFlow和OOM异常。
java堆
java堆是java虚拟机管理的最大一块内存,被线程共享。虚拟机垃圾收集器的主要区域。
作用:几乎所有的对象内存空间都在堆上分配。
特点:
1、从内存回收的角度看,由于现在的垃圾收集器基本采用分代回收,java堆还可以细分为老年代和新生代,新生代又可以分为eden、FromSurvivor和ToSurvivor。
(图示所说的永久代,在hotspot虚拟机中就是方法区。但两者并不等价,只是HotSpot将gc分代收集扩展至方法区,或者说用永久代来实现方法区)
2、从内存分配角度看,线程共享的java堆中可能划分出多个线程共有的缓冲区(Thread Local Allocation Buffer,TLAB)。
无论从哪个角度看,java堆都是存储的对象实例,进一步划分都是为了更好的分配和回收内存。
方法区
方法区和java堆一样,也是线程共享的区域。
作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
特点:同java堆一样不需要连续的内存和可以选择固定大小或者可扩展;可以选择不实现垃圾收集;相对而言,方法区的垃圾收集比较少见,但是并不保证“永久”存在。方法区的垃圾回收目标主要是针对常量池的回收和对类型的卸载(相对类加载而言)。
运行时常量池
方法区的一部分。.class文件中除了类信息外,还有一项就是常量池,用于存放编译器形成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。java虚拟机对class文件格式有着严格的规范要求,只有符合规范的class文件才能被虚拟机装载和执行,但对于运行时常量池,java虚拟机规范没有做任何要求,不同实现的虚拟机可以自己实现这个内存区域。
运行时常量池相对于class文件常量池的另一个特征就是具备动态性,运行期间也可以将新的常量置于池中,并非一定是class文件中的常量。比如String类的intern方法,
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。
JVM内存分配策略
java的自动内存管理最终解决了两个问题:给对象分配内存和回收分配的内存。在具体分析内存分配策略之前,我们需要了解对象的内存布局。
对象的内存布局
Jvm 规范并没有规定对象的存储方式,以下都是以hotSpot虚拟机为例
对象在内存中的存储的布局可以分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头
对象头包括两部分信息:
第一部分为Mark Word,用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据在32位和64位虚拟机(未开启指针压缩)分别为32bit和64bit,Mark Word是一个非固定长度的数据结构,可以根据对象的状态复用自己的存储空间。
另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据。
实例数据
对象真正存储的有效信息,也是程序代码中定义的各种类型的字段内容。无论是从父类继承的还是子类定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数和字段在java源码中定义顺序的影响。
对齐填充
这部分不是必须的,仅仅是为了满足jvm的字节对齐要求
JAVA的内存分配与回收策略
对象优先分配在Eden区。
大多数情况下,对象在新生代Eden区分配,Eden区空间不足时,虚拟机将发起一次MinorGC。
大对象直接进入老年代。
大对象对虚拟机内存分配来说是个坏消息,因为在内存空间还有较多剩余时,为了放置大对象,会提前触发GC。如果大量产生“短命”大对象会引起频繁GC。为此虚拟机提供了一个-XX:PretenureSizeThresHold参数,使得大于这个值的对象直接进入老年代,这样做的目的是避免Eden区及两个Survivor区之间发生大量的内存复制(Eden区采用标记-复制算法进行GC)。
长期存活的对象将进入老年代。
虚拟机为每个对象都定义了一个对象年龄计数器,如果对象在Eden出生并且在第一次MinorGC中存活下来,就将年龄计数器+1,对象每熬过一次GC,年龄就增加1,当年龄增加到一个阈值,就会被分配到老年代。
动态年龄判定。
虚拟机也并不是一定要求必须要达到年龄阈值才将对象放置于老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保。
新生代采用复制收集算法来进行GC,为了内存利用率,只保留一个Survivor空间作为轮换备份,因此当minorGC后仍然有大量对象存活的话,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。