深入理解JVM:内存区域
运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
注:白色
区域为 线程私有(生命周期与线程相同)
,蓝色
区域为 线程共享
。
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,可以被看做是当前线程所执行的字节码的行号指示器。每条线程都需要一个独立的程序计数器,所以这类内存区域是
线程私有
的内存。如果线程正在执行的是
Java
方法,计数器记录的是正在执行的虚拟机字节码指令地址,如果是Native
方法,则计数器值为空(Undefined)。Java 虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈描述的是
Java
方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。本地方法栈(Native Method Stack)
本地方法不是用
Java
实现,对待这些方法需要特别处理。与
Java虚拟机栈
类似,它们之间的区别只不过是本地方法栈为本地方法服务。Java 堆(Java Heap)
Java堆
是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。是垃圾收集的主要区域(所以也叫 “GC 堆”),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把
Java堆
分成以下三块:新生代(Young Generation)
老年代(Old Generation)
永久代(Permanent Generation)
当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:
Eden 空间
From Survivor 空间
To Survivor 空间
Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
方法区(Method Area)
方法区用于存储已被虚拟机加载的
类信息
、常量
、静态变量
、即时编译器编译后的代码
等数据。和Java堆
一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出OutOfMemoryError 异常
。对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,
HotSpot 虚拟机
把它当成永久代
来进行垃圾回收。运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。
Class
文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。除了在编译期生成的常量,还允许动态生成,例如
String
类的intern()
。这部分常量也会被放入运行时常量池。直接内存(Direct Memory)
直接内存不是虚拟机运用时数据去的一部分,也不是Java虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
HotSpot虚拟机
对象的创建
限于普通
Java
对象,不包括数组和Class
对象等检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载,解析,初始化,如果没有,先加载。
为新生对象在
java
堆中分配内存,java
堆如果规整,分配内存的方法可使用“指针碰撞”
,如果不规整,则使用“空闲列表”
。选择哪种分配方式是根据这个虚拟机所采用的垃圾收集器是否带有压缩整理功能决定的。例如,在使Serial、ParNew
等带Compact
过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS
这种基于Mark-Sweep
算法的收集器时,通常采用空闲列表。指针碰撞(Bump the Pointer)
Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。空闲列表(Free List)
Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
内存分配完之后,虚拟机需要将分配到的内存空间都初始化为零值。如果用
TLAB
,则在TLAB
分配时进行。虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、 如何才能找到类的元数据信息、 对象的哈希码、 对象的
GC
分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。执行
init
方法,即按照程序员的意愿进行初始化。
注意:
对象创建在并发情况下不是线程安全的,解决办法:一种是使用同步(CAS);另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在
Java
堆中预先分配一小块内存,即本地线程分配缓冲(Thread Local Allocation Buffer TLAB)。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
对象的内存布局
对象头(Header)
存储对象自身的运行时数据
- 官方名称:Mark Word
- 内容举例:
- 哈希码HashCode
- GC分代年龄
- 锁状态标识
- 线程持有的锁等
- 长度(未开启压缩指针下):32 or 64 位的虚拟机中分别为 32bit or 64 bit
- 非固定数据结构: 考虑到存储效率,已让其在及小空间内存储更多信息。也就是说,在这么多bit下,哪几位存储哪些内容是不定的
类型指针
- 作用:对象指向它的类元数据的指针,JVM通过此来确定该对象是哪个类的实例
并非所有JVM的实现都需要这个指针,也就是对象的元数据查找并不一定要经过对象本身
- 作用:对象指向它的类元数据的指针,JVM通过此来确定该对象是哪个类的实例
实例数据(Instance Data)
- 作用:对象真正存储的有效信息,也是在代码中所定义的各种类型的字段内容
- 内容:无论是从父类继承下来的,还是在子类中定义的,都要记录
- 存储顺序(HotSpot)
- 策略:同宽度者分配到一起
- 默认分配策略:
- longs/doubles
- ints
- shorts/chars
- bytes/booleans
- oop(Ordinary Object Pointers)
对齐填充(Padding)
并不必然存在,占位符的作用。HotSpot要求对象其实地址为8字节的整数倍,也就是要求对象大小是8字节的整数倍。当对象实例数据部分没有对齐时,通过对齐填充来补全。
对象的访问定位
句柄访问
指针访问
比较
使用句柄访问的最大好处是
reference
中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象非常普遍)时只会改变句柄中的实例数据指针,而reference
本身不需要改变使用直接指针访问的最大好处是速度更快,它节省了一次指针定位的时间开销。由于对象的访问在
Java
中非常频繁,因此此类开销积少成多后也是一项非常可观的执行成本。
HotSpot采用的此种方式