1. 运行时数据区
1.1 程序计数器
每条线程都有自己的程序计数器,各条线程之间计数器互不影响,独立存储
1.2 Java虚拟机栈
每个线程start的时候都会创建一个虚拟机栈,每个方法执行的时候虚拟机为其创建栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。方法调用到执行完毕都是栈中入栈到出栈的过程
线程的创建数量是与随着栈内存的增多而减少的:这也很容易理解,线程会有一个私有的虚拟机栈,栈内存越大,虚拟机可以创建的线程数越少。
栈中的局部变量表
基本数据类型,对象引用(reference类型,指向堆内对象的一个指针或者是一个句柄)和returnAddress类型(一条字节码指令)
局部变量槽
局部变量表中的存储空间用局部变量槽表示,long和double类型占两个槽,其余都占用一个槽。编译期间所需内存空间就已经分配完毕,方法执行期间不会改变局部变量表的大小。
异常
超出最大深度,Stack Overflow
动态扩展时扩展失败 OOM
1.3 本地方法栈
为JVM使用本地的C/C++代码服务。
异常
超出最大深度,Stack Overflow
动态扩展时扩展失败 OOM
1.4 Java堆
各个线程共享的区域,此区域存在的唯一目的是存放对象实例。几乎所有的对象实例和数组都应该在堆上分配。
- 比如刚才提到的虚拟机栈局部变量表reference类型,指向的就是堆中的对象实例(JDK1.8之后,MataSpace中的类变量和class对象一块也放在堆中)
此区域也是垃圾收集器工作主要的区域。
异常
动态扩展时扩展失败 OOM
1.5 方法区(JDK1.8之后的MataSpace)
各个线程共享的区域,存储被虚拟机加载的类型信息、常量、静态变量、及时编译器后的本地代码缓存。
垃圾收集线程也会在此区域工作,主要就是为了对常量池的回收和对类型的卸载。
异常
无法分配新内存给新调用的本地方法时 OOM
1.6 运行时常量池
是方法区的一部分,存放编译器生成的字面量和符号引用,这些内容在类加载后存放到方法区的运行时常量池中。
动态性
不一定只有编译时产生常量,运行期间也可以放入新的常量,String intern()
异常
无法为想新放入的常量申请到内存时 OOM
1.7 直接内存
Java NIO引入基于通道和缓冲区的IO方式,可以让本地方法直接分配堆外内存。然后靠存储在Java堆里的DirectByteBuffer对象(存储的是这块地址的指针)操作这块内存
2. 对象
2.1对象创建
new关键字来创建对象,整个过程如下所述:
2.1.1 new指令
-
首先检查支个指令的参数是否可以在常量池中定位到一个类的符号引用
-
查看这个符号引用代表的类是否已经被加载、解析和初始化(若没有则要先执行类加载过程)
-
分配内存。对象所需的内存大小可以在类加载完成后完全确定。分配内存时需要考虑操作的原子性,虚拟机采用CAS配上失败重试(自旋)保证操作的
- 指针碰撞法:假设内存全部规整,由一个指针的左侧是所有正在使用的内存,右边是空闲的,此时指针右移对象大小。
- 空闲列表:内存碎片,需要虚拟机维护记录那块内存可用的列表,在列表中查找特定大小的内存
采用哪种算法是由垃圾收集算法决定的。
-
padding(64位虚拟机,8字节对齐)
-
将除了对象头的部分都初始化为默认值。
-
调用构造函数,为实例变量赋值(就是普通变量)
int a; 实例变量=普通变量
static int b; 类变量=静态变量
对象创建完毕!
2.2 对象的内存布局
2.2.1 对象头
Markword(运行时数据、4B)
哈希码、GC年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
类型指针(4B)
如果是数组的话还会有额外的部分存储数组长度。
2.2.2 实例数据
该是啥是啥(调用构造函数时候,才赋值的哦,刚刚申请内存的时候这块是默认值,0、0.0、false、null)
2.2.3 对齐填充
8字节对齐
2.3 对象的访问定位
通过在栈上的reference类型操作堆上的具体对象。