线程私有的
- 程序计数器
可以看做当前线程所执行的字节码的行号指数器
存在的原因:Java中的多线程是通过切换线程,分配处理器执行时间来实现的,当切换线程后,为了后面可以恢复到正确的执行位置(即切换之前的位置),就需要各个线程各自维护一个独立的程序计数器
该区域是Java虚拟机中唯一一个不会抛出OOM的区域 - Java虚拟机栈
描述Java方法执行的内存模型,每个方法执行时, 都会创建一个栈帧,存储局部变量表等等信息。
每个方法从调用-结束,对应着栈帧从虚拟机入栈-出栈的过程
当进入一个方法时,该方法在帧这种需要的局部变量空间大小是完全确定的,在方法运行期间,不会再改变局部变量表的大小
抛出异常:
- StackOverflowError:请求的栈深度大于虚拟机所允许的深度
- OOM:虚拟机栈可动态扩展,但是扩展时无法申请到足够的内存
- 本地方法栈
线程共享的
- Java堆(GC堆)
唯一目的就是存放对象实例,该区域为垃圾收集器管理的主要区域
该堆可以处于物理不连续的内存空间中,只需要保证逻辑上连续即可;
当堆中内存不足,又无法再次扩展时,会抛出OOM异常 - 方法区
存储已经被虚拟机加载的类信息,常量,静态变量等数据
该区与“永久代”并不等价,有的虚拟机使用永久代实现方法区,方便像管理Java堆一样管理该部分内存
该区域内存回收的主要目标是针对常量池的回收和对类型的卸载
对象的创建过程
遇到一条new指令之后的执行步骤:
- 检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查该该类是否已经被加载,解析和初始化过,若没有,则先执行相应的加载过程
- 检查通过后,则会为新生成的对象分配内存,所需内存大小在类加载完成之后,就可以完全确定了
注意:根据Java堆中内存是否规整,有两种分配方式:
- 指针碰撞(完全规整时):维护一个指针,指示内存用过与否的分界线,分配时,只需要移动指针即可
- 空闲列表(不规整):维护一个列表,记录可用内存,分配时,在表中找出足够大的内存划分给实例,并更新列表
根据虚拟机所采用的垃圾收集器是否带有压缩整理功能,决定选择以上哪种分配方式
处理上述问题外,还有一个问题,在虚拟机中对象创建是非常频繁的行为,因此在并发情况下,修改指针指向并不是线程安全的,有下面两种方式保证线程安全:
- 对分配空间的动作进行同步处理,保证更新操作的原子性;CAS+失败重试
- 为不同线程划分不同内存空间,每个空间称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有当TLAB用完并需要分配新的TLAB时,才需要同步锁定。
- 分配完成,需要将分配到的内存空间都初始化为零值,保证对象实例字段在Java代码中不赋值就可以使用
- 从虚拟机视角来看,一个新的对象已经产生,但是从程序来看,还必须执行init方法,将对象按照程序员的意愿进行初始化
对象的访问定位
程序通过栈上的reference数据来操作堆上的具体对象,该reference对象为一个指向对象的引用,至于该引用如何定位并访问堆中对象的具体位置,有以下两种方式
- 使用句柄
堆中需要划分一块内存作为句柄池,池中包含对象的实例数据与类型数据各自的具体地址信息
优点:reference中存储稳定的句柄地址,当对象被移动时,只需要改变句柄中的实例数据指针 - 直接指针
堆中放置对象类型数据,而reference中存放对象地址
优点:速度快