什么是Java虚拟机内存结构?
从 JVM 角度看
若从内存分布看
方法区也叫元数据区(元空间),是从 jdk1.8 开始的的,在 jdk1.8 之前叫作永久代,它的内存是放在 JVM 虚拟机数据区的,但是从 jdk1.8 开始,元空间放在堆外内存。
程序计数器
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,所以它也是线程私有的。 如果线程正在执行的是 Java 方法,这个计数器记录的是正在执行的字节码指令地址;若当前线程正在执行的是一个本地方法,那么此时程序计数器为Undefined
。 这个区域是 java 虚拟机中唯一一个不会发生OutOfMemoryError
的区域。
Java 虚拟机栈(Java 方法栈)
Java 虚拟机栈的定义
虚拟机栈描述的是 java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。同时,它也是线程私有的,它的生命周期与线程相同。
压栈出栈过程
如上图所述,Java 虚拟机栈每个线程的栈顶是当前正在执行的活动栈,就是当前正在执行的方法,程序计数器也会指向这个地址。 只有活动的方法的本地变量可以被操作栈使用,当这个方法调用另一个方法,与之对应的栈帧有会被创建,新创建的栈帧压入栈顶,变为当前活动站。 方法结束后,当前栈帧被弹出,方法的返回值变成新的活动栈帧中操作栈的一个操作数。
局部变量表
定义为一个数字数组,主要用于存储方法参数、定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及 return address 类型。 在编译期间,局部变量表所需要的内存空间就已经完成分配了。所以当进入方法时,这个栈帧需要分配的局部变量值已经被确定了,方法运行期间也不会改变局部变量表的大小。 局部变量空间(Slot)分配规则(64 位占 2 个,32 位占 1 个):
- long 和 double 类型的数据会占用 2 个 Slot
- 其余的数据类型占用 1 个 Slot
对于 slot 的理解:
- JVM 虚拟机会为局部变量表中的每个 slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 this,会存放在 index 为 0 的 slot 处,其余的参数表顺序继续排列。
- 栈帧中的局部变量表中的槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的。 在栈帧中,与性能调优关系最密切的部分,就是局部变量表,方法执行时,虚拟机使用局部变量表完成方法的传递局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈
- 栈顶缓存技术:由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理 CPU 的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
- 每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好。32bit 类型占用一个栈单位深度,64bit 类型占用两个栈单位深度操作数栈。
- 并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问。
Java 虚拟机栈的特点
-
运行速度特别快,仅仅次于 PC 寄存器。
-
局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。
-
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常。
- OutOfMemoryError 若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常。
-
Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁。
-
出现 StackOverFlowError 时,内存空间可能还有很多。
本地方法栈
本地方法栈(Native Method Stacks)和 Java 虚拟机栈非常类似,区别是本地方法栈是为执行 Native 方法服务的,而 Native 方法一般是 c 写的,所以也叫 c 栈。
Java 堆
堆是用来存放对象实例的内存空间,几乎所有的对象实例都在这里分配内存。
- 堆是线程共享的,所有线程都是访问同一个堆
- 堆的生命周期和虚拟机是一致的,虚拟机创建时就创建了堆
- 堆是垃圾收集器管理的主要区域,所以有时候也叫“GC 堆”
- Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
- 堆既可以是固定大小,也可以是可扩展的,当前主流虚拟机的堆都是可扩展的(通过-Xmx 和-Xms 控制)
- 堆中如果没有内存可以完成实例分配,并且无法再扩展时,抛出 OutOfMemoryError 异常
方法区
方法区是用来存储 已加载的类信息、常量、静态变量、即时编译后的代码等数据,虽然虚拟机规范中把方法区描述为堆的一个逻辑部分,但是它也叫 Non-Heap(非堆)。
- 方法区是线程共享的,因为它是堆的一个逻辑部分。
- 一般不会被垃圾回收,虽然在方法区中实现了 GC 回收,但是方法区中的信息一般需要长期存在,所以在此区域回收效率低,一般不会对此区域回收。主要回收的目标:对常量池的回收,对类型的卸载。
- 在 Java 8 之前方法区使用永久代实现,从 Java 8 开始是使用元空间(Matespace)实现。
- 当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。 运行时常量池 方法区中存放的常量就存放在运行时常量池中。 当类被 Java 虚拟机加载后, .class 文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如 String 类的
intern()
方法就能在运行期间向常量池中添加字符串常量。
直接内存(堆外内存)
直接内存是虚拟机以外的内存,不是 Java 虚拟机定义的内存区域,但是这部分内存也可以被 Java 使用。
操作直接内存
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 虽然直接内存分配不受 Java 堆大小的限制,但是受总内存大小的限制。所以在配置堆内存(-Xmx)等参数时,需要考虑到直接内存。
对象访问(如何进行访问对象?)
Object obj = new Object();
假设这句代码出现在方法体中,涉及到的内存分配如下:
- Java 方法栈:“Object obj”这部分会反应到 Java 方法栈的本地变量表中,作为 reference 类型存在,保存一个指向堆内存对象的引用。
- 堆:“new Object()” 这部分会反应到 Java 堆中,保存 Object 类型所有的实例数据值的结构化内存
- 方法区:对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息 在 Java 虚拟机中的表示