1.1运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,他们有各自的用途以及创建销毁时间,一些区域随着虚拟机进程的启动一直存在,有一些则是依赖用户线程的建立和销毁。
其中云形状的为线程共享数据区,其他为线程隔离数据区
1.1.1 程序计数器
内存空间较小,当前线程所执行字节码的行号指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器。
每条线程私有,如果线程执行是一个Java方法计数器记录的是正在执行的虚拟机字节码指令地址;如果是本地方法,则计数器值为空,该区域是唯一一个没有OutOfMemoryError的区域
1.1.2 Java虚拟机栈
线程私有,每个方法被执行,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数表、动态连接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程。
局部变量表存放基本数据类型,这些数据类型的存储空间以局部变量槽来表示,long和double占两个变量槽。
如果线程请求的栈深度大于虚拟机允许的栈深度抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,无法申请到足够内存时抛出OutOfMemoryError异常
1.1.3 本地方法栈
与虚拟机栈类似只不过,本地方法栈服务于本地方法
1.1.4 Java堆
内存中最大,线程共享,目的是存放对象的实例,所有的对象实例以及数组都应当在堆上分配。
所有线程共享的Java堆可以划分出多个线程私有的缓冲区,主流的Java虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms参数来设定),堆无法扩展时抛出OutOfMemoryError异常
1.1.5 方法区
线程共享,用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等信息,不需要连续的内存,可选择固定大小和可扩展,还可以选择不实现垃圾收集。
当方法区无法满足新的内存分配需求时,抛出OutOfMemoryError异常
1.1.6 运行时常量池
是方法区的一部分,类信息中的常量池表存放在这里。当常量池无法再申请到内存时抛出OutOfMemoryError异常
1.1.7 直接内存
直接内存不是运行时数据区的一部分,目的是避免在Java堆和Native中来回复制数据,会出现OutOfMemoryError异常
1.2 HotSpot虚拟机对象探秘
1.2.1 对象创建(普通Java对象)
- 检查这个指令的参数是否能在常量池中定位到一个类的引用,并且检查这个类是否已被加载、解析和初始化过,如果没有则先进行类加载过程。
- 为对象分配内存,按照堆的内存是否规整,分为‘’指针碰撞法‘’和‘’空闲列表法‘’,堆是否规整取决于使用的垃圾收集器是否有压缩功能。
除划分空间外还有多线程并发问题,指针可能同时被多个线程修改,两种解决方案,一种是CAS加上失败重试,另一种每个线程预先分配一小块内存,称为本地线程缓冲区,线程分配对象优先在缓冲区分配,简称TLAB法,可以通过-XX:+/-UseTLAB参数设定。 - 初始化内存空间(不包括对象头)为0值,保证实例字段不赋值也能使用。
- 设置对象头信息,例如哈希码,GC分代信息
- 执行Class文件中的方法,进行初始化
1.2.2 对象的内存布局
对象在堆中分三个部分:对象头、实例数据和对齐填充。
- 对象头包括两类信息,一类用于存储对象自身的运行时数据,例如:哈希码、GC分代信息等,成为‘’Mark Word‘’,它被设计成有着动态定义的数据结构,结构如下
另一类信息是类型指针,虚拟机通过这个指针来判断该对象是哪个类的实例,但并不是所有的对象数据都有类型指针。
- 实例数据部分是对象真正的有效信息
- 对齐填充起着占位符的作用,对象必须是8字节的整数倍
1.2.3 对象访问定位
访问对象主流有两种方式,使用句柄和直接指针
- 如果使用句柄访问,堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含对象实例数据与类型数据各自的地址信息。
- 如果使用直接指针访问,reference中存储的直接是对象地址,如果只是访问对象本身,就不需要一次间接访问的开销
这两种各有优势,句柄访问最大的好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针;而使用直接访问的好处是速度更快,它节省了一次指针定位的时间开销