JVM运行时内存数据区域
前言
JVM会在执行过程中把它所管理的内存花费为若干个不同的数据区域。如下图所示
下面分别对这些区域进行解释。
1、程序技术器
- 概念:程序技术器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 作用:
- 字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- JVM的多线程是通过线程轮流切换并分配处理器执行时间的方法实现的。在任意一个时刻,一个内核都只会执行一条线程中的指令。因此为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,线程之间的技术器互不影响,独立存储。因此这类内存区域是线程私有的。
- 其他:如果线程执行的是一个java方法,这个技术器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器为空,此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的地方。
2、Java虚拟机栈
- 概念:虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量,操作数栈,动态链接,方法出口等消息。每一个方法从调用至执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 生命周期: 与其线程相同。
- 是否线程私有:是
- 局部变量表:存放了编译器可知的基本数据类型,对象引用和指向一条字节码指令的地址。其中64位长度的long和double占用2个局部变量空间,其余类型只占有一个。
- 异常:
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。换言之线程请求的栈容量超过栈允许的最大容量。常见的就是递归调用没有正确终止,导致栈溢出。
- OutOfMemoryError:虚拟机栈动态扩展时如果无法申请到足够的内存空间,就会抛出OutOfMemoryError异常。
3、本地方法栈
- 概念:本地方法栈与虚拟机栈所发挥的作用是非常相似的。两者之间的区别在于虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。其他均是一样的。
4、Java堆
- 概念:java Heap 是java虚拟机所管理的内存中最大的一块。
- 是否线程私有: 否,所有线程共享的一块内存区域。
- 生命周期:在虚拟机启动的时候创建。
- 目的与作用:存放对象实例,几乎所有的对象实例都是在这里分配内存。是垃圾收集器管理的主要区域。
- 细分:从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为新生代,老年代。再细致可以分为Eden空间,From Survivor空间,To Survivor空间等。
- 参数控制:
-Xms JVM启动时申请的最小堆内存,默认为物理内存的1/64但小于1G
-Xmx JVM启动时申请的最大堆内存,默认为物理内存但1/4但小于1G
-XX:MinHeapFreeRadio 默认当剩余堆内存空间小于40%时,JVM会将-Xms会增大到-Xmx的大小,通过该参数可以指定这个比例
-XX:MaxHeapFreeRadio 默认当空余堆内存大于70%时,JVM会减小堆内存至-Xms大小,通过该参数可以指定这个比例
5、方法区
- 概念:方法区存放了要加载的类的消息,类中的静态变量,final定义的常量,类中的field方法信息,对象中的getName,isInterface等方法的所需数据均是来源于方法区。
- 是否线程私有:否
- 对于HotSpot虚拟机来说,很多人更愿意将方法区称为永久代,这因为GC分代收集扩展至方法区,或者说是使用永久代(Permanent Generation)实现方法区。这个区域的内存回收目标主要是针对常量池的回收(字符常量池已经移出永久代)和类型的卸载。永久代在java8中已经被完全移除,原先永久代中类的元信息会被放入本地内存(元数据区,metaspace),将类的静态变量和内部字符串放入java堆中。
- Metaspace:默认情况下,类元数据只受可用的本地内存限制,通过参数 -XX:MaxMetaspaceSize可以限制本地内存分配给类元数据的大小,若没有指定这个参数,元空间会在运行时根据需要动态调整。对于僵死的类及加载器的垃圾回收将在元数据使用达到MaxMetaspaceSize时进行GC。
- 异常:OutOfMemoryError异常。 当方法区无法满足内存分配需求时会抛出该异常。
6、运行时常量池
- 运行时常量池是方法区的一部分。主要用于存放编译期生成的各种字面量和符号引用。这部分内容将会在类加载后进入方法区的运行时常量池中存放。并不是要求常量一定只用在编译的时候才能缠手,运行期间也可能将新的常量放入池中,例如String.intern()方法。
- 异常:当常量池无法再申请到内存时会抛出OutOfMemoryError。
7、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但是这部分内存也会被频繁调用,而且可能导致OutOfMemoryError异常。
在NIO类中引入了基于Channel与Buffer的I/O方式,其可以使用Native函数库直接分配堆外内存。然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样避免了在Java堆与Native堆中来回复制数据。
直接内存的分配不会受到Java堆大小的限制,但是内存一定会受到本机总内存大小以及处理器寻址空间的限制。千万不要在设置-Xmx时忽略直接内存,从而使得各个内存区域总和大于物理内存限制,导致在进行动态扩展时出现OutOfMemoryError异常
对象探秘
对象的创建
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用是否已经被加载、解析和初始化过。若没有则先执行类加载过程。在类加载通过后,虚拟机将为新生对象分配内存,相当于是从Java堆中划出来一部分。目前有两种分配方法。
- 指针碰撞。要求Java堆内存中是绝对规整的,所用用过的内存在一边,空闲的在另一边,中奖放着一个指针作为分界点的指示器。分配内存就是把指针向空闲空间那边挪动一段与对象大小相等的距离。
- 空闲列表。堆内存并不是规整的,使用的内存和空闲的内存相互交错,虚拟机维护了一个列表,记录了哪些内存块是可用的。
Java堆是否规整,取决于垃圾收集器是否带有压缩整理功能决定的。因此在使用Serial,ParNew等带Compact过程的收集器时,采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用的是空闲列表。
由于修改指针所指向的内存地址在并发情况下,不是线程安全的。目前有两种方案解决这个问题。
- 对分配内存空间的动作进行同步处理,虚拟机采用CAS配上失败重试的方法保证更新的原子性。
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上,只用TLAB用完,分配新的TLAB时,才需要同步锁定。
内存分配完成后,虚拟机将内存空间初始化为0,然后对对象进行必要的设置,设置信息存放在对象头。最后调用init方法得到一个可用的对象。
对象的内存布局
对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对齐填充。
对象头
对象头信息分为两部分.
- 第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码,对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID,偏向时间戳,对象分代年龄 | 01 | 可偏向 |
- 对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 对齐填充不是必然存在的,也没有特别的含义。
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。
-
使用句柄访问,在java堆中会划分出一块内存用来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中则包含了对象实例数据与类型数据各自的具体地址信息。
-
使用地址直接访问,此时reference中存储的直接就是对象的地址。
句柄的优势:reference中存储的是稳定的句柄地址,在对象的位置发生改变的时候,只会改变句柄中的实例数据指针,而reference不会受到影响。
直接指针访问优势:速度更快,减少了一个指针定位的时间开销。