运行时数据区域
JVM
内存区域也称为Java
运行时数据区域。其中包括:程序计数器、虚拟机栈、本地方法栈、堆、静态方法区、静态常量池等
共享的:方法区+堆
私有的:虚拟机栈+本地方法栈+程序计数器
- 为什么区分堆和栈
- 因为结构化语言里函数(子程序)调用最方便的实现方式就是用栈,以至于现在绝大部分芯片都对栈提供芯片级的硬件支持,一条指令即可搞定栈的pop操作。栈的好处是:方便、快、有效避免内存碎片化。栈的问题是:不利于管理大内存(尤其在16位和32位时代)、数据的生命周期难于控制(栈内的有效数据通常是连续存储的,所以pop时后申请的内存必须早于先申请的内存失效),所以栈不利于动态地管理并且有效地利用宝贵的内存资源。于是我们有了堆。。。
作者:知乎用户
链接:https://www.zhihu.com/question/49927441/answer/118535159
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
程序计数器
Program Counter Register
- 是一个比较小的内存空间
- 当前线程所执行字节码的行号指示器
- 字节码指示器工作时候就是通过改变这个计数器的值来选取下一个需要执行的字节码指令
- 一个处理器内核只会执行一条线程中的指令,为了切换线程的方便,每个线程都有独立的程序计数器,即线程私有
- 生命周期与线程相同,随
JVM
启动而生,JVM
关闭而死。 - 如果处理器执行的是Java方法,这个计数器记录正在执行的字节码的地址,
- 如果执行的是本地(native)方法,则这个计数器应为空undefined.
- 唯一在
Java
虚拟机规范中没有规定任何OutOfMemoryError
情况区域。
虚拟机栈
- 线程私有
- 生命周期同线程相同
描述java方法执行的线程内存模型
每个方法被执行时候,java虚拟机都会同步一个栈帧用于存放
局部变量表
- (每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现)
- 局部变量表在编译期间分配内存空间,可以存放编译期的各种变量类型
- 基本数据类型 :
boolean
,byte
,char
,short
,int
,float
,long
,double
等8
种; - 对象引用类型 :
reference
,指向对象起始地址的引用指针; - 返回地址类型 :
returnAddress
,返回地址的类型。
- 基本数据类型 :
操作数栈
- 虚拟机在操作数栈中可存储的数据类型:
int
、long
、float
、double
、reference
和returnType
等类型 (对于byte
、short
以及char
类型的值在压入到操作数栈之前,也会被转换为int
)。 - 和局部变量表不同的是,它不是通过索引来访问,而是通过标准的栈操作 — 压栈和出栈来访问。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
- 虚拟机在操作数栈中可存储的数据类型:
- 动态链接
- 每个**栈帧**都包含一个指向运行时**常量池**中所属的**方法的引用**,持有这个引用是为了支持方法调用过程中的**动态链接**。
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
- 方法出口等信息
- **正常返回**:当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为**正常完成出口**(`Normal Method Invocation Completion`),一般来说,调用者的`PC`**计数器**可以作为返回地址。
- **异常返回**:当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为**异常完成出口**(`Abrupt Method Invocation Completion`),返回地址要通过**异常处理器**表来确定。
1. 恢复**上层方法**的**局部变量表**和**操作数栈**。
1. 把**返回值**压入**调用者栈帧**的**操作数栈**。
1. 将`PC`**计数器**的值指向**下一条**方法指令位置。
- 每个方法被调用到执行完毕的过程,就对应着一个栈帧入栈到出栈的过程。
- 异常
- 其一:如果当前线程请求的栈深度大于虚拟机栈所允许的深度,将会抛出
StackOverflowError
异常(在虚拟机栈不允许动态扩展的情况下); - 其二:如果扩展时无法申请到足够的内存空间,就会抛出
OutOfMemoryError
异常。
- 其一:如果当前线程请求的栈深度大于虚拟机栈所允许的深度,将会抛出
本地方法栈
- 虚拟机栈作用:为虚拟机执行Java方法服务
- 本地方法栈作用:为虚拟机执行本地方法(Native)服务
- 与虚拟机栈一样,本地方法栈也会抛出
StackOverflowError
和OutOfMemoryError
异常。
堆
Java
堆是被所有线程共享的最大的一块内存区域- 在虚拟机启动时创建
- 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 被垃圾收集器管理
- 新的对象分配是首先放在年轻代 (
Young Generation
) 的Eden
区,Survivor
区作为Eden
区和Old
区的缓冲,在Survivor
区的对象经历若干次收集仍然存活的,就会被转移到老年代Old
中。
- 新的对象分配是首先放在年轻代 (
- 可以划分出多个线程私有的分配缓冲区
- 为了更好的回收内存+分配内存
- 物理上可以不连续,但逻辑上需要连续
Out Of Memory的几种情况
- 内存泄漏+内存溢出是什么
- 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
- 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
- 几种情况分析
- 堆溢出,不断的创建对象,并且对象没有回收
“java.lang.OutOfMemoryError:java heap space”
- 栈溢出,如果虚拟机在扩展栈时无法申请足够的内存空间,则抛出OutOfMemoryError异常(程序在不断的创建线程,这可能会产生OutOfMemoryError异常,但是此种情况与栈空间是否足够大并没有任何关系
“java.lang.OutOfMemoryError:unable to create new native thread”
)- 在单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常
-
方法区
共享
- 用于存储类信息、常量、静态常量和即时编译后的代码等数据。
运行时常量池
- 是方法区的一部分
Class
文件中除了有类的版本、字段、方法和接口等描述信息外, 还有一类信息是常量池表,用于存储编译期间生成的各种字面量和符号引用。
直接内存
直接内存不属于虚拟机运行时数据区的一部分,也不是Java
虚拟机规范中定义的内存区域。 Java NIO
允许Java
程序直接访问直接内存,通常直接内存的速度会优于Java堆内存。因此,对于读写频繁、性能要求高的场景,可以考虑使用直接内存。
变量的位置
类变量(就是类中被static修饰的变量)在java8以后会随着Class对象一起放到Java堆中,这个时候“类变量在方法区”就变成了一种逻辑上的表述。(P272)了。
字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。
classA{
// 字符串常量池在堆
private String a = “aa”; // a 为成员变量的引用,在堆区,“aa”为未经 new 的常量,在常量池
publicboolean methodB() {
String b = “bb”; // b 为局部变量的引用,在栈区,“bb”为未经 new 的常量,在常量池
final String c = “cc”; // c 为局部变量的引用,在栈区,“cc”为未经 new 的常量,在常量池
}
}
对象探秘
对象创建
- 遇到
字节码new
指令的时候,先去检查这个指令的参数能否在常量池中定位到这个类的符号引用
,并且检查这个符号引用代表的类是否被加载、解析、初始化过。 - 为新生对象**分配内存**。
- 对象所需内存大小在类加载完以后就可确定。分配空间就相当于把一块确定大小的内存从java堆中划分出来(移动内存指针)。
- 分配内存时候在并发时候不安全:给对象A分配内存,指针还没来得及修改,对象B同时使用原来的指针来分配内存
- 对内存分配空间的动作进行同步处理—-采用
CAS+失败
重试保证更新的原子性 - 把内存分配的动作按照线程划分在不同的空间中进行,每个线程预先分配一下块内存,作为
本地线程分配缓冲TLAB
,只有这块内存用完了,分配新缓冲区才需要同步锁定。
- 对内存分配空间的动作进行同步处理—-采用
- 虚拟机将分配到的内存空间(不包括对象头)**初始化为0**
- 保证了对象的实例字段在java代码可以不赋初始值就可以直接使用。
- 虚拟机对对象进行一系列的设置,如所属类的元信息、对象的哈希码、对象GC分带年龄 、线程持有的锁 、偏向线程ID 等信息。这些信息存储在对象头 (
Object Header
)。
完成上述操作,从虚拟机来看,一个对象就已经产生了。
但是从java程序的视角来看,对象创建刚刚开始。
- 此时构造函数还没有执行,所有字段都是默认的零值,对象所需要的其他资源和状态信息也还没有按照预定的意图构造好。
- 一般来说,new指令之后回接着执行init方法,按照程序员的意愿进行初始化,这样一个真正的对象才算构造出来。
对象内存布局
对象在堆内存的存储空间可以划分为三部分:
- 对象头(
Header
)- 包括两类信息
- 存储对象的运行时数据:
- 如哈希码(hashCode)、GC分带年龄、线程持有的锁、偏向线程ID 等信息。
- 这部分数据的长度在
32
位和64
位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32
个和64
个Bit
,官方称它为“Mark Word”
。
- 类型指针:
- 对象指向它的类型元数据的指针
- 通过这个指针确定这个对象是那个类的实例
- 如果对象是java数组,那么对象头还必须有一块记录数组长度的数据。
- 存储对象的运行时数据:
- 包括两类信息
- 实例数据(
Instance Data
)- 真正有效的数据
- 无论是从父类继承下来的还是该类自身的,都需要记录下来,而这部分的存储顺序受虚拟机的分配策略和定义的顺序的影响
- 分配策略总是按照字节大小由大到小的顺序排列,相同字节大小的放在一起
- 默认:long/double -> int/float -> short/char -> byte/boolean -> reference
- 设置了
-XX:FieldsAllocationStyle=0
(默认是1
),那么引用类型数据就会优先分配存储空间:- reference -> long/double -> int/float -> short/char -> byte/boolean
- 对齐填充(
Padding
)HotSpot
虚拟机要求每个对象的起始地址必须是8
字节的整数倍,也就是对象的大小必须是8
字节的整数倍。- 而对象头部分正好是
8
字节的倍数(32
位为1
倍,64
位为2
倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。
对象的定位访问
程序回通过栈上的reference数据来操作堆上的具体对象。
通过reference的对象访问方式有两种:
- 使用句柄:java堆划分出一块内存作为句柄池,reference存储的就是对象的句柄地址,句柄中包含了对象的示例数据与类型数据各自的地址信息。
- 优点:存储的是稳定的句柄地址,对象移动的时候只改变句柄中的实例数据指针,reference本身不需要被修改。
- 缺点:两次访存
- 直接指针(HotSpot默认的):reference存储的就是对象的地址,访问对象本身不需要多义词间接访问开销。
- 优点:速度快,节省了一次指针定位的时间
实战OutOfMemery异常
堆溢出
public class HeapOOM {
/*
堆的最大最小值 以及 生成堆快照
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public static void main(String[] args) {
ArrayList<HeapOOM> heapOOMS = new ArrayList<>();
while (true) {
heapOOMS.add(new HeapOOM());
}
}
}
虚拟机栈 与 本地方法栈 溢出
HotSpot并不区分虚拟机栈以及本地方法栈