运行时数据区域
java 虚拟机的内存区域有各自的创建和销毁时间,如,栈,程序计数器依赖于线程的启动,而方法区和堆随着 java 虚拟机的启动而存在。
程序计数器
程序计数器可以理解为,保存当前字节码执行的行号,cpu 按照程序计数器的指示,有序地执行代码逻辑。当线程恢复时,也需要程序计数器指示,线程被挂起前的执行位置。
因此,每个线程都有一个程序计数器,来保证线程切换后的恢复。
如果执行的是 native 方法,程序计数器为空。
java 虚拟机栈
虚拟机栈也是线程私有的区域,程序运行时,执行方法的调用之前,会将所有参数入栈,被调用的方法,按照所需要的参数个数,从栈中取用。方法调用完毕,则会出栈。
异常情况
- 线程请求的栈深度大于虚拟机允许的最大深度,如递归,死循环(方法迟迟无法执行完毕,参数不断入栈)
- 动态扩展时申请不到足够的内存
本地方法栈
本地方法栈用于存放 native 方法的参数
java 堆
用于存放对象实例,java 堆是线程共享的。
java 堆中的区域还能再细分,比如新生代,老年代。划分的目的主要是为了内存回收。
异常情况:OOM
- 内存泄露:对象得不到回收,无法释放已申请的内存空间
- 内存溢出:申请不到足够的空间
方法区
与 java 堆一样,属于线程共享的区域,存放编译后的代码数据,如加载的类信息,常量,静态变量等。
常量池
属于方法区的一部分
HotSpot 虚拟机对象
内存分配
分配方式
- 指针碰撞:假设堆中内存规整,用过的放在一边,没用过的放另一边,那么内存的分配就是移动指针,找到一块没用的区域。
- 空闲列表:虚拟机维护一张列表,记录哪些内存块可用。
java 堆是否规整,取决于垃圾收集器。
线程安全
- 对分配内存的动作进行同步处理(原子性操作)
- 将内存分配的动作按照线程划分在不同的空间中执行
对象的内存布局
对象在内存中的布局可以分为:对象头,实例数据,对齐填充
对象头
- 储存运行时的自身数据,如哈希码,锁状态标志
- 类型指针,指向元数据
实例数据
对象真正存储的有效信息,代码中定义的各种类型的字段内容
对齐填充
占位符
对象的访问定位
对象的访问方式由虚拟机决定
- 句柄:java 堆中划分出句柄池,reference 中指向句柄地址,句柄包含对象的实例数据和类型数据地址
- 直接指针:reference 直接指向对象的实例数据,而实例数据中包含类型数据的地址
使用句柄的好处是,句柄比较稳定,如果对象被移动,只需要修改句柄而不用修改 reference,使用指针的好处是速度快,节省了指针定位的时间开销。