Java内存划分
- 方法区
- 堆
- 栈
- 虚拟机栈
- 本地方法栈(C语言:native方法)
- 程序计数器
1 程序计数器
是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 每个线程都有一个独立的程序计数器,各个线程相互不影响,独立存储,这类内存区域叫做“线程私有”的内存。
- 如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址。
- 如果执行的是一个 naive 方法,计数器的值为空。
- 此内存区域是唯一一个没有规定OOM的区域。
2 Java虚拟栈
- Java虚拟栈与程序计数器一样,也是线程私有的,生命周期跟线程相同。
- 每个方法执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出入口信息。
- 局部变量表存放了编译器可知的各种基本数据类型、对象引用。
- 如果线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常。
- 如果虚拟机无法申请到足够的内存,将OOM。
3 本地方法栈
- 除了执行的是 native 方法外,其他的与虚拟栈基本相同。
4 Java堆
- 新生代和老年代。
- Eden 空间、From Survivor 空间、To Survivor 空间。
- 主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制);
- 所有线程共享。
5 方法区
- 所有线程共享的。
- 存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 对于HotSpot 虚拟机,也常被叫做“永久代”。
- 运行时的常量池是方法区的一部分。
6 对象的创建
当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,必须执行相应类的加载过程。
- 指针碰撞 内存分配方式
假如堆内存是觉得完整的,所有用过的内存都放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,分配内存就是将指针移动一段与内存相等的距离。
- 空闲列表 内存分配方式
内存是不完整的,已使用和空闲的相互交错,虚拟机维护一个列表,记录哪些内存可用,再分配时找到一块足够大小的内存空间给对象实例,并更新列表上的记录。
7 虚拟机保证并发
- 通过CAS配上失败重试的方法保证更新操作的原子性。
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。
8 对象的内存布局
分为三块区域:
- 对象头(Header)
- 第一部分用于存储对象本身的运行时数据
- 另一部分类型指针,即对象指向它的类元数据的指针
- 虚拟机通过这个指针来确定这个对象是哪个类的实例
- 并不是所有的虚拟机都必须在对象数据上保留类型指针
- 数组的元数据是无法确定数组的大小的
- 实例数据(Instance Data)
- 对齐填充(Padding)
9 对象的访问定位
通过栈上的reference数据来操作堆上的具体对象。
- 句柄访问
Java堆会划分出一块内存来作为句柄池,reference中存储的就是对象中的句柄地址,句柄中包含对象实例数据与类型数据各自的具体地址。
优势:reference中存储的是稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针,reference本身不会改变。 - 指针访问
Java堆对象的布局中就必须考虑如何放置访问数据的相关信息,reference中存储的直接就是对象的地址。
优势:速度快。Hotspot 虚拟机使用。