1、运行时数据区
数据区按照线程使用情况分两大类:
- 由所有线程共享的数据区:字面意思,所有线程都使用同一个。
堆和方法区、存储类的静态数据和对象数据、需要垃圾回收
- 线程隔离的数据区:每个线程有自己的,例如:每个线程都有自己的程序计数器
虚拟机栈、本地方法栈、程序计数器、不需要垃圾回收
1.1、程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,线程私有,如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
1.2、Java虚拟机栈
线程私有,是Java方法执行的线程内存模型,每个方法执行的时候,会创建一个栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法被调用就会有一个栈帧入栈,执行完毕则出栈。
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
1.3、本地方法栈
本地方法栈和虚拟机栈类似,区别是虚拟机栈执行的是Java方法,本地方法栈执行的是本地方法服务。Hot-Spot虚拟机直接就把本地方法栈和虚拟机栈合二为一。
1.4、堆
1.5、方法区
方法区各个线程共享的区域,用于存储已被虚拟机加载的类型信息、常量、静态变量,即时编译后的代码缓存等数据。
1.6、运行时常量池
运行时常量池时方法区的一部分,
2、对象的创建、布局、访问
2.1、对象的创建
在运行时数据区中,我们知道方法区中会存储类的信息,所以当用new关键字创建对象的时候,首先会检查这个常量池是否有类符号引用,并检查这个符号引用代表的类是否以为加载,如果没有则会进行类加载过程。
类加载检查通过后,虚拟机将为新生对象分配内存,对象所需的内存大小在类加载完成后就可以确定。
给对象分配内存可以根据内存是否规整分为两类:
- 内存规整:指针碰撞
当使用Serial、ParNew等带压缩整理过程的收集器时,垃圾回收后,会把使用过的内存放在一边,没有使用过的放在另一边,这样的话,在中间放一个指针作为分配内存的指示器,分配内存时把对象放在没有使用的内存,指针移动对象大小的位置。 - 内存不规整:空闲列表
当使用CMS这种基于清楚算法的收集器时,就会采用空闲列表的方式,空闲列表是虚拟机维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够的内存划分给对象,更新列表的值。
分配内存时如何保证线程安全:
- 对分配内存操作做同步处理-CAS
- 对每个线程预先分配一小块内存(Thread Local Allocation Buffer TLAB),缓冲区用完了才需要同步操作,是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
在虚拟机中对象就算完成了,但是对于Java程序来看,后面还有初始化,执行Init方法等。
2.2、对象的内存布局
对象可以划分为三个部分:对象头、实例数据、对齐填充
2.2.1、对象头
对象头分为两个部分:
Mark Word:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,在32位和64位的机器中分别位32个bit和64个bit
类型指针:对象执行类型元数据的指针
实例数据:真正存放对象数据的部分。
对齐填充:hotspot自动内存管理要求任何对象的大小都是8字节的倍数。所有需要对齐部分凑够8字节的倍数。
2.3、对象的访问定位
访问方式主要有两种:句柄、直接指针
直接指针:如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址。