运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域,不同的区域有不同的用途以及创建和销毁时间;
有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。
- 内存区域
- 栈(Stack)
- 堆(Heap)
- 方法区(Method Area)
- 本地方法区(Native Method Area)
- 程序计数器(Program Counter Register)
栈
特点:
* 先进后出,后进先出
* 线程私有,内存大小固定,且连续
* 一个线程创建一个栈,栈和线程的生命周期是同步的
* 一个进程可以有多个线程,即一个进行可以创建多个栈
* 栈中不存在垃圾回收问题,如果存在垃圾,则程序将无法正常运行
* 服务于Java的方法
* 存取速度快
关键词:
* 栈帧:是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic linking)、方法返回值和异常分派(Dispatch Exception),栈帧随着方法的调用而创建,随着方法的结束而销毁 **注意:无论方法是正常完成还是异常完成(抛出了在方法内未捕获的异常)都算作方法结束**
* 入栈(压栈):向栈中添加元素的过程叫做入栈(压栈)
* 出栈(弹栈):从栈中取出元素的过程叫做出栈(弹栈)
* 父帧:指向本方法的出口地址(上层方法正在执行的地址)
* 子帧:指向内层方法的入口
存储内容:
* 局部变量表(字面值)
* 动态链接
* 方法出口
* 操作数栈等
每个线程创建一个栈,栈内部存储的信息如图所示:
栈的方法调用过程以及出栈和入栈的过程如图所示:
队列和栈的对比:
* 相同点
* 队列和栈都是被用来存储数据的
* 不同点:
* 操作的名称不同:队列的插入称为"入队",队列的删除称为"出队";栈的插入称为"进栈",栈的删除称为"出栈"
* 可操作的方式不同:队列入队在队尾,出队在队头,队头和队尾都可操作;栈的进栈和出栈都在栈顶进行,无法对栈底进行操作
* 操作的方法不同:队列是先进先出(FIFO);栈是后进先出(LIFO)
面试题:
开发中你遇到过的异常有哪些?
*Error:申请一个固定大小的Java栈,此时一个线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,就会报出栈溢出错误,例如递归无出口情况下
OutOfMemoryError:当Java虚拟机栈动态扩展的情况下,无法申请到足够的内存,或者在创建新线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机就会抛出一个内存溢出的错误
*Error和OutOfMemoryError详细分析在后面会分析
堆
特点:
* 一个JVM只有一个堆内存,且物理内存空间不一定是连续的
* 线程共享,堆内存的大小可以调节
* 需要动态分配内存,存取速度慢
* 垃圾存在的地方,从垃圾回收的角度可将堆分为:新生代和老年代
存储内容:
* 创建好的对象(静态的对象也是存在于堆)
* 创建好的数组
堆栈的区别:
* 物理地址
* 堆的物理地址分配对对象是不连续的,因此性能慢,在GC时,考虑到不连续分配,所以有各种算法
* 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的,所以性能快
* 内存分别
* 堆因为是不连续的,且需要在运行时动态分配内存,因此堆内存大小不固定,一般堆内存远大于栈内存
* 栈是连续的,且生存期和大小需要在编译时就确认,因此栈内存大小是固定的
* 存放的内容
* 堆存放的是对象的实例和数组,因此堆更关注的是数据的存储
* 栈存放的是局部变量、操作数栈、返回值、动态链接等,因此栈更关注的是程序方法的执行
* 共享范围
* 堆对于整个应用程序都是共享可见的
* 栈对于本线程是可见的,对于其他线程是不可见的
方法区(永久代/元空间)
特点:
* 线程共享的,内存大小可调节
* 物理内存空间可以是不连续的
* 内存的大小决定了系统保存的类的多少
* 所有字段和方法字节码,以及一些特殊方法,如构造函数、接口代码在此定义,简单说,所有定义的方法的信息都保存在该区域
存储内容
* 类型信息
* 对每个加载的类型(类Class、接口interface、枚举enum、注解annotation),JVM必须在方法区存储以下类型信息
1. 这个类型的完整有效名称(全限定名=包名.类名(例如:java.lang.String))
2. 这个类型直接父类的完整有效名(除interface和java.lang.object这种没有父类的)
3. 这个类型的修饰符(public、abstract、final的某个子集)
4. 这个类型直接接口的一个有序列表
* 运行时常量池
* 方法区中存在的运行时常量池是通过.class字节码文件内部包含的常量池加载以后,会将常量池中的字面量与符号引用存放到方法区的运行时常量池中,并将符号地址转为真实地址。
* 静态变量
* 1.8以后存储在普通的堆中
* JIT代码缓存
* 域信息
* JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
* 域相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)
* 方法信息
* JVM必须保存所有的方法信息,同域信息一样,包括声明的顺序
* 方法名称
* 方法的返回类型
* 方法的参数数量和类型(按顺序)
* 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的每一个子集)
* 异常表(abstract和native方法除外)
* 异常表存储:每个异常的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
**注:实例变量存在堆内存中,与方法区无关**
永久代和元空间的区别:
* 相同点:
* 永久代和元空间都是用于存储类、方法等相关类信息的(存储目的相同)
* 不同点:
* 实现
* 永久代是JDK8以前的方法区实现
* 元空间是JDK8以后的方法区实现
* 存储内存空间不同
* 永久代存储于JVM内存中
* 元空间存储于本地内存中
* 存储内存的大小不同
* 由于存储位置不同,永久代存储于JVM内存中,且JVM内存大小有限,很容易造成OOM(内存溢出)
* 元空间存储于本地内存,如果不设置方法区的大小,默认将耗尽所有可用本地内存,比永久代更不容易造成OOM
* 调节参数:
* 永久代
* -XX:PermSize:设置初始的永久代大小
* -XX:MaxPermSize:设置永久代的最大值
* 元空间
* -XX :MetaspaceSize:设置初始的元空间大小
* -XX :MaxMetaspaceSize:设置元空间的最大值
注:由于方法区存储的目的是为了长久的存储类信息和运行时常量池信息,因此一般不会对方法区进行GC垃圾回收
本地方法区
特点:
* 只存放与本地方法接口交互有关的信息(Native修饰的方法)
* 线程私有
* 服务于本地方法调用
* 可扩展Java的方法
存储内容:
* native修饰的方法
本地方法区:在源码中实现是调用了C++底层代码,而我们可以通过native关键字实现语言的扩展,调用Python等语言
程序计数器
程序计数寄存器(Program Counter Register)本质上是一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
正在执行Java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是执行native方法,计数器的值为空。
这个内存区域是唯一一个虚拟机中没有规定任何OutOfMemoryError情况的区域
程序计数寄存器并不是物理寄存器,JVM中的PC寄存器是对物理寄存器的一种抽象模拟
PC寄存器常见问题:
1.使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停切换各个线程,当线程切换回来以后,当前线程不知道执行到哪,需要使用PC寄存器来明确从哪里继续执行
2.PC寄存器为什么会被设定为线程私有?
所谓的多线程都是在某一个特定的时间段内指挥执行其中某一个线程的方法,CPU需要不停的切换任务,这样必然会导致线程中断,当恢复的时候如何确保当前线程继续执行,就只能通过一个其他线程无法修改,且自己唯一的PC寄存器,来记录中断前执行的指令地址